summaryrefslogtreecommitdiff
path: root/java/com/android/incallui
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/incallui')
-rw-r--r--java/com/android/incallui/AccelerometerListener.java173
-rw-r--r--java/com/android/incallui/AndroidManifest.xml121
-rw-r--r--java/com/android/incallui/AnswerScreenPresenter.java110
-rw-r--r--java/com/android/incallui/AnswerScreenPresenterStub.java44
-rw-r--r--java/com/android/incallui/AudioModeProvider.java69
-rw-r--r--java/com/android/incallui/Bindings.java52
-rw-r--r--java/com/android/incallui/CallButtonPresenter.java515
-rw-r--r--java/com/android/incallui/CallCardPresenter.java1110
-rw-r--r--java/com/android/incallui/CallerInfo.java573
-rw-r--r--java/com/android/incallui/CallerInfoAsyncQuery.java638
-rw-r--r--java/com/android/incallui/CallerInfoUtils.java279
-rw-r--r--java/com/android/incallui/ConferenceManagerFragment.java106
-rw-r--r--java/com/android/incallui/ConferenceManagerPresenter.java139
-rw-r--r--java/com/android/incallui/ConferenceParticipantListAdapter.java523
-rw-r--r--java/com/android/incallui/ContactInfoCache.java759
-rw-r--r--java/com/android/incallui/ContactsAsyncHelper.java269
-rw-r--r--java/com/android/incallui/ContactsPreferencesFactory.java56
-rw-r--r--java/com/android/incallui/DialpadFragment.java461
-rw-r--r--java/com/android/incallui/DialpadPresenter.java91
-rw-r--r--java/com/android/incallui/ExternalCallNotifier.java465
-rw-r--r--java/com/android/incallui/InCallActivity.java756
-rw-r--r--java/com/android/incallui/InCallActivityCommon.java820
-rw-r--r--java/com/android/incallui/InCallCameraManager.java173
-rw-r--r--java/com/android/incallui/InCallOrientationEventListener.java194
-rw-r--r--java/com/android/incallui/InCallPresenter.java1679
-rw-r--r--java/com/android/incallui/InCallServiceImpl.java99
-rw-r--r--java/com/android/incallui/InCallUIMaterialColorMapUtils.java67
-rw-r--r--java/com/android/incallui/Log.java145
-rw-r--r--java/com/android/incallui/ManageConferenceActivity.java86
-rw-r--r--java/com/android/incallui/NotificationBroadcastReceiver.java165
-rw-r--r--java/com/android/incallui/PostCharDialogFragment.java96
-rw-r--r--java/com/android/incallui/ProximitySensor.java292
-rw-r--r--java/com/android/incallui/StatusBarNotifier.java842
-rw-r--r--java/com/android/incallui/ThemeColorManager.java142
-rw-r--r--java/com/android/incallui/TransactionSafeFragmentActivity.java64
-rw-r--r--java/com/android/incallui/VideoCallPresenter.java1289
-rw-r--r--java/com/android/incallui/VideoPauseController.java416
-rw-r--r--java/com/android/incallui/answer/bindings/AnswerBindings.java29
-rw-r--r--java/com/android/incallui/answer/impl/AffordanceHolderLayout.java178
-rw-r--r--java/com/android/incallui/answer/impl/AndroidManifest.xml3
-rw-r--r--java/com/android/incallui/answer/impl/AnswerFragment.java981
-rw-r--r--java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java127
-rw-r--r--java/com/android/incallui/answer/impl/CreateCustomSmsDialogFragment.java137
-rw-r--r--java/com/android/incallui/answer/impl/PillDrawable.java43
-rw-r--r--java/com/android/incallui/answer/impl/SmsBottomSheetFragment.java136
-rw-r--r--java/com/android/incallui/answer/impl/affordance/AndroidManifest.xml3
-rw-r--r--java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java642
-rw-r--r--java/com/android/incallui/answer/impl/affordance/SwipeButtonView.java505
-rw-r--r--java/com/android/incallui/answer/impl/affordance/res/values/dimens.xml23
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/AndroidManifest.xml3
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/AnswerMethod.java45
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/AnswerMethodFactory.java52
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java47
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java1149
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java496
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/TwoButtonMethod.java268
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/drawable/call_answer.xml19
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/drawable/circular_background.xml6
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/layout/swipe_up_down_method.xml115
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/layout/two_button_method.xml97
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/values-h240dp/values.xml20
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/values-h280dp/dimens.xml21
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/values-h480dp/dimens.xml20
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/values/dimens.xml27
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/values/ids.xml5
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/values/strings.xml14
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/values/styles.xml7
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/values/values.xml25
-rw-r--r--java/com/android/incallui/answer/impl/classifier/AccelerationClassifier.java99
-rw-r--r--java/com/android/incallui/answer/impl/classifier/AnglesClassifier.java193
-rw-r--r--java/com/android/incallui/answer/impl/classifier/AnglesPercentageEvaluator.java33
-rw-r--r--java/com/android/incallui/answer/impl/classifier/AnglesVarianceEvaluator.java42
-rw-r--r--java/com/android/incallui/answer/impl/classifier/Classifier.java35
-rw-r--r--java/com/android/incallui/answer/impl/classifier/ClassifierData.java96
-rw-r--r--java/com/android/incallui/answer/impl/classifier/DirectionClassifier.java37
-rw-r--r--java/com/android/incallui/answer/impl/classifier/DirectionEvaluator.java23
-rw-r--r--java/com/android/incallui/answer/impl/classifier/DurationCountClassifier.java35
-rw-r--r--java/com/android/incallui/answer/impl/classifier/DurationCountEvaluator.java39
-rw-r--r--java/com/android/incallui/answer/impl/classifier/EndPointLengthClassifier.java36
-rw-r--r--java/com/android/incallui/answer/impl/classifier/EndPointLengthEvaluator.java42
-rw-r--r--java/com/android/incallui/answer/impl/classifier/EndPointRatioClassifier.java43
-rw-r--r--java/com/android/incallui/answer/impl/classifier/EndPointRatioEvaluator.java42
-rw-r--r--java/com/android/incallui/answer/impl/classifier/FalsingManager.java140
-rw-r--r--java/com/android/incallui/answer/impl/classifier/GestureClassifier.java31
-rw-r--r--java/com/android/incallui/answer/impl/classifier/HistoryEvaluator.java115
-rw-r--r--java/com/android/incallui/answer/impl/classifier/HumanInteractionClassifier.java142
-rw-r--r--java/com/android/incallui/answer/impl/classifier/LengthCountClassifier.java39
-rw-r--r--java/com/android/incallui/answer/impl/classifier/LengthCountEvaluator.java45
-rw-r--r--java/com/android/incallui/answer/impl/classifier/Point.java95
-rw-r--r--java/com/android/incallui/answer/impl/classifier/PointerCountClassifier.java51
-rw-r--r--java/com/android/incallui/answer/impl/classifier/PointerCountEvaluator.java23
-rw-r--r--java/com/android/incallui/answer/impl/classifier/ProximityClassifier.java97
-rw-r--r--java/com/android/incallui/answer/impl/classifier/ProximityEvaluator.java28
-rw-r--r--java/com/android/incallui/answer/impl/classifier/SpeedAnglesClassifier.java147
-rw-r--r--java/com/android/incallui/answer/impl/classifier/SpeedAnglesPercentageEvaluator.java33
-rw-r--r--java/com/android/incallui/answer/impl/classifier/SpeedClassifier.java40
-rw-r--r--java/com/android/incallui/answer/impl/classifier/SpeedEvaluator.java36
-rw-r--r--java/com/android/incallui/answer/impl/classifier/SpeedRatioEvaluator.java39
-rw-r--r--java/com/android/incallui/answer/impl/classifier/SpeedVarianceEvaluator.java36
-rw-r--r--java/com/android/incallui/answer/impl/classifier/Stroke.java72
-rw-r--r--java/com/android/incallui/answer/impl/classifier/StrokeClassifier.java28
-rw-r--r--java/com/android/incallui/answer/impl/hint/AndroidManifest.xml13
-rw-r--r--java/com/android/incallui/answer/impl/hint/AnswerHint.java46
-rw-r--r--java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java133
-rw-r--r--java/com/android/incallui/answer/impl/hint/DotAnswerHint.java283
-rw-r--r--java/com/android/incallui/answer/impl/hint/EmptyAnswerHint.java39
-rw-r--r--java/com/android/incallui/answer/impl/hint/EventAnswerHint.java235
-rw-r--r--java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java30
-rw-r--r--java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java118
-rw-r--r--java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java67
-rw-r--r--java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_large.xml4
-rw-r--r--java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_mid.xml4
-rw-r--r--java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_small.xml5
-rw-r--r--java/com/android/incallui/answer/impl/hint/res/layout/dot_hint.xml30
-rw-r--r--java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml36
-rw-r--r--java/com/android/incallui/answer/impl/hint/res/values/dimens.xml12
-rw-r--r--java/com/android/incallui/answer/impl/hint/res/values/strings.xml5
-rw-r--r--java/com/android/incallui/answer/impl/res/anim/incoming_unlocked_icon_entry.xml19
-rw-r--r--java/com/android/incallui/answer/impl/res/anim/incoming_unlocked_text_entry.xml9
-rw-r--r--java/com/android/incallui/answer/impl/res/layout/fragment_avatar.xml26
-rw-r--r--java/com/android/incallui/answer/impl/res/layout/fragment_custom_sms_dialog.xml14
-rw-r--r--java/com/android/incallui/answer/impl/res/layout/fragment_incoming_call.xml152
-rw-r--r--java/com/android/incallui/answer/impl/res/values-h240dp/dimens.xml21
-rw-r--r--java/com/android/incallui/answer/impl/res/values-h300dp/dimens.xml20
-rw-r--r--java/com/android/incallui/answer/impl/res/values-h480dp/dimens.xml22
-rw-r--r--java/com/android/incallui/answer/impl/res/values-h540dp/dimens.xml21
-rw-r--r--java/com/android/incallui/answer/impl/res/values/dimens.xml25
-rw-r--r--java/com/android/incallui/answer/impl/res/values/strings.xml26
-rw-r--r--java/com/android/incallui/answer/impl/utils/FlingAnimationUtils.java293
-rw-r--r--java/com/android/incallui/answer/impl/utils/Interpolators.java30
-rw-r--r--java/com/android/incallui/answer/protocol/AnswerScreen.java38
-rw-r--r--java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java44
-rw-r--r--java/com/android/incallui/answer/protocol/AnswerScreenDelegateFactory.java23
-rw-r--r--java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java150
-rw-r--r--java/com/android/incallui/answerproximitysensor/AnswerProximityWakeLock.java37
-rw-r--r--java/com/android/incallui/answerproximitysensor/PseudoProximityWakeLock.java85
-rw-r--r--java/com/android/incallui/answerproximitysensor/PseudoScreenState.java66
-rw-r--r--java/com/android/incallui/answerproximitysensor/SystemProximityWakeLock.java90
-rw-r--r--java/com/android/incallui/async/PausableExecutor.java56
-rw-r--r--java/com/android/incallui/async/PausableExecutorImpl.java40
-rw-r--r--java/com/android/incallui/audioroute/AndroidManifest.xml3
-rw-r--r--java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java114
-rw-r--r--java/com/android/incallui/audioroute/res/drawable-hdpi/ic_phone_audio_grey600_24dp.pngbin0 -> 990 bytes
-rw-r--r--java/com/android/incallui/audioroute/res/drawable-mdpi/ic_phone_audio_grey600_24dp.pngbin0 -> 632 bytes
-rw-r--r--java/com/android/incallui/audioroute/res/drawable-xhdpi/ic_phone_audio_grey600_24dp.pngbin0 -> 1297 bytes
-rw-r--r--java/com/android/incallui/audioroute/res/drawable-xxhdpi/ic_phone_audio_grey600_24dp.pngbin0 -> 1979 bytes
-rw-r--r--java/com/android/incallui/audioroute/res/layout/audioroute_selector.xml37
-rw-r--r--java/com/android/incallui/audioroute/res/values/strings.xml7
-rw-r--r--java/com/android/incallui/audioroute/res/values/styles.xml14
-rw-r--r--java/com/android/incallui/autoresizetext/AndroidManifest.xml25
-rw-r--r--java/com/android/incallui/autoresizetext/AutoResizeTextView.java316
-rw-r--r--java/com/android/incallui/autoresizetext/res/values/attrs.xml47
-rw-r--r--java/com/android/incallui/baseui/BaseFragment.java75
-rw-r--r--java/com/android/incallui/baseui/Presenter.java54
-rw-r--r--java/com/android/incallui/baseui/Ui.java20
-rw-r--r--java/com/android/incallui/bindings/ContactUtils.java33
-rw-r--r--java/com/android/incallui/bindings/DistanceHelper.java36
-rw-r--r--java/com/android/incallui/bindings/InCallUiBindings.java48
-rw-r--r--java/com/android/incallui/bindings/InCallUiBindingsFactory.java26
-rw-r--r--java/com/android/incallui/bindings/InCallUiBindingsStub.java81
-rw-r--r--java/com/android/incallui/bindings/PhoneNumberService.java77
-rw-r--r--java/com/android/incallui/call/CallList.java763
-rw-r--r--java/com/android/incallui/call/DialerCall.java1401
-rw-r--r--java/com/android/incallui/call/DialerCallDelegate.java25
-rw-r--r--java/com/android/incallui/call/DialerCallListener.java39
-rw-r--r--java/com/android/incallui/call/ExternalCallList.java136
-rw-r--r--java/com/android/incallui/call/InCallServiceListener.java40
-rw-r--r--java/com/android/incallui/call/InCallUiLegacyBindings.java26
-rw-r--r--java/com/android/incallui/call/InCallUiLegacyBindingsFactory.java26
-rw-r--r--java/com/android/incallui/call/InCallUiLegacyBindingsStub.java24
-rw-r--r--java/com/android/incallui/call/InCallVideoCallCallback.java197
-rw-r--r--java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java279
-rw-r--r--java/com/android/incallui/call/TelecomAdapter.java160
-rw-r--r--java/com/android/incallui/call/VideoUtils.java151
-rw-r--r--java/com/android/incallui/commontheme/AndroidManifest.xml3
-rw-r--r--java/com/android/incallui/commontheme/res/animator/button_state.xml30
-rw-r--r--java/com/android/incallui/commontheme/res/animator/disabled_alpha.xml22
-rw-r--r--java/com/android/incallui/commontheme/res/color/incall_button_ripple.xml5
-rw-r--r--java/com/android/incallui/commontheme/res/color/incall_button_white.xml5
-rw-r--r--java/com/android/incallui/commontheme/res/drawable-hdpi/ic_phone_audio_white_36dp.pngbin0 -> 1010 bytes
-rw-r--r--java/com/android/incallui/commontheme/res/drawable-mdpi/ic_phone_audio_white_36dp.pngbin0 -> 682 bytes
-rw-r--r--java/com/android/incallui/commontheme/res/drawable-xhdpi/ic_phone_audio_white_36dp.pngbin0 -> 1362 bytes
-rw-r--r--java/com/android/incallui/commontheme/res/drawable-xxhdpi/ic_phone_audio_white_36dp.pngbin0 -> 2259 bytes
-rw-r--r--java/com/android/incallui/commontheme/res/drawable-xxxhdpi/ic_phone_audio_white_36dp.pngbin0 -> 3156 bytes
-rw-r--r--java/com/android/incallui/commontheme/res/drawable/answer_answer_background.xml10
-rw-r--r--java/com/android/incallui/commontheme/res/drawable/answer_decline_background.xml10
-rw-r--r--java/com/android/incallui/commontheme/res/drawable/incall_end_call_background.xml10
-rw-r--r--java/com/android/incallui/commontheme/res/values-w260dp-h520dp/dimens.xml21
-rw-r--r--java/com/android/incallui/commontheme/res/values-w520dp-h260dp-land/dimens.xml21
-rw-r--r--java/com/android/incallui/commontheme/res/values/colors.xml5
-rw-r--r--java/com/android/incallui/commontheme/res/values/dimens.xml22
-rw-r--r--java/com/android/incallui/commontheme/res/values/strings.xml35
-rw-r--r--java/com/android/incallui/commontheme/res/values/styles.xml58
-rw-r--r--java/com/android/incallui/contactgrid/AndroidManifest.xml3
-rw-r--r--java/com/android/incallui/contactgrid/BottomRow.java142
-rw-r--r--java/com/android/incallui/contactgrid/ContactGridManager.java315
-rw-r--r--java/com/android/incallui/contactgrid/TopRow.java168
-rw-r--r--java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml71
-rw-r--r--java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_top_row.xml26
-rw-r--r--java/com/android/incallui/contactgrid/res/values/ids.xml31
-rw-r--r--java/com/android/incallui/contactgrid/res/values/strings.xml69
-rw-r--r--java/com/android/incallui/hold/AndroidManifest.xml3
-rw-r--r--java/com/android/incallui/hold/OnHoldFragment.java102
-rw-r--r--java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml46
-rw-r--r--java/com/android/incallui/hold/res/values/strings.xml6
-rw-r--r--java/com/android/incallui/incall/bindings/InCallBindings.java28
-rw-r--r--java/com/android/incallui/incall/impl/AndroidManifest.xml3
-rw-r--r--java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java135
-rw-r--r--java/com/android/incallui/incall/impl/ButtonChooser.java114
-rw-r--r--java/com/android/incallui/incall/impl/ButtonChooserFactory.java100
-rw-r--r--java/com/android/incallui/incall/impl/ButtonController.java584
-rw-r--r--java/com/android/incallui/incall/impl/CheckableLabeledButton.java286
-rw-r--r--java/com/android/incallui/incall/impl/InCallButtonGridFragment.java137
-rw-r--r--java/com/android/incallui/incall/impl/InCallFragment.java501
-rw-r--r--java/com/android/incallui/incall/impl/InCallPagerAdapter.java59
-rw-r--r--java/com/android/incallui/incall/impl/MappedButtonConfig.java193
-rw-r--r--java/com/android/incallui/incall/impl/res/animator/incall_button_elevation.xml31
-rw-r--r--java/com/android/incallui/incall/impl/res/color/incall_button_icon.xml5
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable-mdpi/ic_addcall_white.pngbin0 -> 708 bytes
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable-xhdpi/ic_addcall_white.pngbin0 -> 1259 bytes
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/incall_button_background.xml22
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/incall_button_background_checked.xml5
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/incall_button_background_more.xml30
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/incall_button_background_unchecked.xml5
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/incall_ic_add_call.xml4
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/incall_ic_dialpad.xml4
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/incall_ic_manage.xml4
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/incall_ic_merge.xml4
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/incall_ic_pause.xml4
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/tab_indicator_default.xml12
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/tab_indicator_selected.xml12
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/tab_selector.xml6
-rw-r--r--java/com/android/incallui/incall/impl/res/layout/call_composer_data_fragment.xml15
-rw-r--r--java/com/android/incallui/incall/impl/res/layout/frag_incall_voice.xml104
-rw-r--r--java/com/android/incallui/incall/impl/res/layout/incall_button_grid.xml77
-rw-r--r--java/com/android/incallui/incall/impl/res/values-h320dp/dimens.xml5
-rw-r--r--java/com/android/incallui/incall/impl/res/values-h385dp/dimens.xml5
-rw-r--r--java/com/android/incallui/incall/impl/res/values-h480dp/dimens.xml4
-rw-r--r--java/com/android/incallui/incall/impl/res/values-h580dp/dimens.xml4
-rw-r--r--java/com/android/incallui/incall/impl/res/values-h580dp/styles.xml24
-rw-r--r--java/com/android/incallui/incall/impl/res/values-w260dp-h520dp/dimens.xml7
-rw-r--r--java/com/android/incallui/incall/impl/res/values-w300dp-h540dp/dimens.xml5
-rw-r--r--java/com/android/incallui/incall/impl/res/values/attrs.xml8
-rw-r--r--java/com/android/incallui/incall/impl/res/values/dimens.xml17
-rw-r--r--java/com/android/incallui/incall/impl/res/values/ids.xml6
-rw-r--r--java/com/android/incallui/incall/impl/res/values/strings.xml56
-rw-r--r--java/com/android/incallui/incall/impl/res/values/styles.xml23
-rw-r--r--java/com/android/incallui/incall/protocol/ContactPhotoType.java35
-rw-r--r--java/com/android/incallui/incall/protocol/InCallButtonIds.java59
-rw-r--r--java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java61
-rw-r--r--java/com/android/incallui/incall/protocol/InCallButtonUi.java50
-rw-r--r--java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java67
-rw-r--r--java/com/android/incallui/incall/protocol/InCallButtonUiDelegateFactory.java23
-rw-r--r--java/com/android/incallui/incall/protocol/InCallScreen.java53
-rw-r--r--java/com/android/incallui/incall/protocol/InCallScreenDelegate.java43
-rw-r--r--java/com/android/incallui/incall/protocol/InCallScreenDelegateFactory.java23
-rw-r--r--java/com/android/incallui/incall/protocol/PrimaryCallState.java114
-rw-r--r--java/com/android/incallui/incall/protocol/PrimaryInfo.java112
-rw-r--r--java/com/android/incallui/incall/protocol/SecondaryInfo.java109
-rw-r--r--java/com/android/incallui/latencyreport/LatencyReport.java140
-rw-r--r--java/com/android/incallui/legacyblocking/BlockedNumberContentObserver.java105
-rw-r--r--java/com/android/incallui/legacyblocking/DeleteBlockedCallTask.java124
-rw-r--r--java/com/android/incallui/maps/StaticMapBinding.java51
-rw-r--r--java/com/android/incallui/maps/StaticMapFactory.java28
-rw-r--r--java/com/android/incallui/res/anim/activity_open_enter.xml43
-rw-r--r--java/com/android/incallui/res/anim/activity_open_exit.xml31
-rw-r--r--java/com/android/incallui/res/anim/decelerate_cubic.xml21
-rw-r--r--java/com/android/incallui/res/anim/decelerate_quint.xml21
-rw-r--r--java/com/android/incallui/res/anim/on_going_call.xml31
-rw-r--r--java/com/android/incallui/res/color/ota_title_color.xml21
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_block_grey600_24dp.pngbin0 -> 518 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_call_end_white_24dp.pngbin0 -> 454 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_call_split_white_24dp.pngbin0 -> 326 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_close_grey600_24dp.pngbin0 -> 225 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_location_on_white_24dp.pngbin0 -> 371 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_01.pngbin0 -> 577 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_02.pngbin0 -> 650 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_03.pngbin0 -> 803 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_04.pngbin0 -> 1009 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_05.pngbin0 -> 946 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_06.pngbin0 -> 856 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_07.pngbin0 -> 577 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_08.pngbin0 -> 577 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_09.pngbin0 -> 577 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_person_add_grey600_24dp.pngbin0 -> 300 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_phone_paused_white_24dp.pngbin0 -> 458 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_question_mark.pngbin0 -> 845 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_schedule_white_24dp.pngbin0 -> 575 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/img_business.pngbin0 -> 3311 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/img_conference.pngbin0 -> 7037 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/img_no_image.pngbin0 -> 5362 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/img_phone.pngbin0 -> 6157 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_block_grey600_24dp.pngbin0 -> 348 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_call_end_white_24dp.pngbin0 -> 315 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_call_split_white_24dp.pngbin0 -> 256 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_close_grey600_24dp.pngbin0 -> 178 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_location_on_white_24dp.pngbin0 -> 265 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_01.pngbin0 -> 375 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_02.pngbin0 -> 401 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_03.pngbin0 -> 501 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_04.pngbin0 -> 638 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_05.pngbin0 -> 572 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_06.pngbin0 -> 548 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_07.pngbin0 -> 375 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_08.pngbin0 -> 375 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_09.pngbin0 -> 375 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_person_add_grey600_24dp.pngbin0 -> 211 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_phone_paused_white_24dp.pngbin0 -> 346 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_question_mark.pngbin0 -> 569 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_schedule_white_24dp.pngbin0 -> 377 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/img_business.pngbin0 -> 2240 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/img_conference.pngbin0 -> 4629 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/img_no_image.pngbin0 -> 3509 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/img_phone.pngbin0 -> 3798 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_block_grey600_24dp.pngbin0 -> 690 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_call_end_white_24dp.pngbin0 -> 534 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_call_split_white_24dp.pngbin0 -> 377 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_close_grey600_24dp.pngbin0 -> 261 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_location_on_white_24dp.pngbin0 -> 456 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_01.pngbin0 -> 730 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_02.pngbin0 -> 806 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_03.pngbin0 -> 1017 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_04.pngbin0 -> 1313 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_05.pngbin0 -> 1218 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_06.pngbin0 -> 1098 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_07.pngbin0 -> 730 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_08.pngbin0 -> 730 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_09.pngbin0 -> 730 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_person_add_grey600_24dp.pngbin0 -> 341 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_phone_paused_white_24dp.pngbin0 -> 584 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_question_mark.pngbin0 -> 1094 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_schedule_white_24dp.pngbin0 -> 737 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/img_business.pngbin0 -> 4759 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/img_conference.pngbin0 -> 9517 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/img_no_image.pngbin0 -> 7369 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/img_phone.pngbin0 -> 8189 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_block_grey600_24dp.pngbin0 -> 1029 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_call_end_white_24dp.pngbin0 -> 736 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_call_split_white_24dp.pngbin0 -> 461 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_close_grey600_24dp.pngbin0 -> 353 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_location_on_white_24dp.pngbin0 -> 675 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_01.pngbin0 -> 1051 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_02.pngbin0 -> 1198 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_03.pngbin0 -> 1524 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_04.pngbin0 -> 2045 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_05.pngbin0 -> 1900 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_06.pngbin0 -> 1675 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_07.pngbin0 -> 1051 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_08.pngbin0 -> 1051 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_09.pngbin0 -> 1051 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_person_add_grey600_24dp.pngbin0 -> 485 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_phone_paused_white_24dp.pngbin0 -> 842 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_question_mark.pngbin0 -> 1686 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_schedule_white_24dp.pngbin0 -> 1107 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/img_business.pngbin0 -> 6499 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/img_conference.pngbin0 -> 16306 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/img_no_image.pngbin0 -> 9850 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/img_phone.pngbin0 -> 10848 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/ic_block_grey600_24dp.pngbin0 -> 1353 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/ic_call_end_white_24dp.pngbin0 -> 929 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/ic_call_split_white_24dp.pngbin0 -> 646 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/ic_close_grey600_24dp.pngbin0 -> 444 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/ic_location_on_white_24dp.pngbin0 -> 869 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/ic_person_add_grey600_24dp.pngbin0 -> 638 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/ic_question_mark.pngbin0 -> 2304 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/ic_schedule_white_24dp.pngbin0 -> 1478 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/img_business.pngbin0 -> 10730 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/img_conference.pngbin0 -> 19584 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/img_no_image.pngbin0 -> 16251 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/img_phone.pngbin0 -> 18635 bytes
-rw-r--r--java/com/android/incallui/res/drawable/img_conference_automirrored.xml21
-rw-r--r--java/com/android/incallui/res/drawable/img_no_image_automirrored.xml21
-rw-r--r--java/com/android/incallui/res/drawable/incall_background_gradient.xml8
-rw-r--r--java/com/android/incallui/res/drawable/spam_notification_icon.xml34
-rw-r--r--java/com/android/incallui/res/drawable/unknown_notification_icon.xml34
-rw-r--r--java/com/android/incallui/res/layout/activity_manage_conference.xml6
-rw-r--r--java/com/android/incallui/res/layout/caller_in_conference.xml119
-rw-r--r--java/com/android/incallui/res/layout/conference_manager_fragment.xml33
-rw-r--r--java/com/android/incallui/res/layout/incall_dialpad_fragment.xml24
-rw-r--r--java/com/android/incallui/res/layout/incall_screen.xml33
-rw-r--r--java/com/android/incallui/res/layout/video_call_lte_to_wifi_failed.xml28
-rw-r--r--java/com/android/incallui/res/values-sw360dp/dimens.xml32
-rw-r--r--java/com/android/incallui/res/values-w500dp-land/colors.xml21
-rw-r--r--java/com/android/incallui/res/values-w500dp-land/dimens.xml23
-rw-r--r--java/com/android/incallui/res/values/animation_constants.xml19
-rw-r--r--java/com/android/incallui/res/values/colors.xml92
-rw-r--r--java/com/android/incallui/res/values/config.xml23
-rw-r--r--java/com/android/incallui/res/values/dimens.xml66
-rw-r--r--java/com/android/incallui/res/values/strings.xml367
-rw-r--r--java/com/android/incallui/res/values/styles.xml80
-rw-r--r--java/com/android/incallui/ringtone/DialerRingtoneManager.java134
-rw-r--r--java/com/android/incallui/ringtone/InCallTonePlayer.java168
-rw-r--r--java/com/android/incallui/ringtone/ToneGeneratorFactory.java34
-rw-r--r--java/com/android/incallui/sessiondata/AndroidManifest.xml18
-rw-r--r--java/com/android/incallui/sessiondata/AvatarPresenter.java31
-rw-r--r--java/com/android/incallui/sessiondata/MultimediaFragment.java231
-rw-r--r--java/com/android/incallui/sessiondata/res/drawable/answer_data_background.xml22
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_frag.xml42
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml50
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml59
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_text.xml43
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_frag.xml61
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml62
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml78
-rw-r--r--java/com/android/incallui/sessiondata/res/values/dimens.xml21
-rw-r--r--java/com/android/incallui/sessiondata/res/values/ids.xml23
-rw-r--r--java/com/android/incallui/sessiondata/res/values/styles.xml24
-rw-r--r--java/com/android/incallui/spam/NumberInCallHistoryTask.java107
-rw-r--r--java/com/android/incallui/spam/SpamCallListListener.java364
-rw-r--r--java/com/android/incallui/spam/SpamNotificationActivity.java483
-rw-r--r--java/com/android/incallui/spam/SpamNotificationService.java132
-rw-r--r--java/com/android/incallui/util/AccessibilityUtil.java35
-rw-r--r--java/com/android/incallui/util/TelecomCallUtil.java51
-rw-r--r--java/com/android/incallui/video/bindings/VideoBindings.java28
-rw-r--r--java/com/android/incallui/video/impl/AndroidManifest.xml3
-rw-r--r--java/com/android/incallui/video/impl/CameraPermissionDialogFragment.java62
-rw-r--r--java/com/android/incallui/video/impl/CheckableImageButton.java222
-rw-r--r--java/com/android/incallui/video/impl/SpeakerButtonController.java118
-rw-r--r--java/com/android/incallui/video/impl/SwitchOnHoldCallController.java91
-rw-r--r--java/com/android/incallui/video/impl/VideoCallFragment.java1215
-rw-r--r--java/com/android/incallui/video/impl/res/color/videocall_button_icon_tint.xml5
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-hdpi/ic_switch_camera.pngbin0 -> 1930 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked.pngbin0 -> 3103 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_disabled.pngbin0 -> 3304 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_pressed.pngbin0 -> 4836 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_default.pngbin0 -> 4209 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_disabled.pngbin0 -> 4022 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_pressed.pngbin0 -> 5695 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-mdpi/ic_switch_camera.pngbin0 -> 1293 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked.pngbin0 -> 1426 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_disabled.pngbin0 -> 1715 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_pressed.pngbin0 -> 2724 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_default.pngbin0 -> 2155 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_disabled.pngbin0 -> 1990 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_pressed.pngbin0 -> 3188 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xhdpi/ic_switch_camera.pngbin0 -> 2518 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked.pngbin0 -> 4603 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_disabled.pngbin0 -> 4957 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_pressed.pngbin0 -> 7213 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_default.pngbin0 -> 6352 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_disabled.pngbin0 -> 6054 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_pressed.pngbin0 -> 8418 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxhdpi/ic_switch_camera.pngbin0 -> 4001 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked.pngbin0 -> 9032 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_disabled.pngbin0 -> 8611 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_pressed.pngbin0 -> 13529 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_default.pngbin0 -> 11101 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_disabled.pngbin0 -> 10736 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_pressed.pngbin0 -> 15167 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxxhdpi/ic_switch_camera.pngbin0 -> 2424 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable/videocall_background_circle_white.xml10
-rw-r--r--java/com/android/incallui/video/impl/res/drawable/videocall_video_button_background.xml27
-rw-r--r--java/com/android/incallui/video/impl/res/layout-v21/switch_camera_button.xml6
-rw-r--r--java/com/android/incallui/video/impl/res/layout/frag_videocall.xml114
-rw-r--r--java/com/android/incallui/video/impl/res/layout/frag_videocall_land.xml111
-rw-r--r--java/com/android/incallui/video/impl/res/layout/switch_camera_button.xml6
-rw-r--r--java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml33
-rw-r--r--java/com/android/incallui/video/impl/res/layout/videocall_controls.xml113
-rw-r--r--java/com/android/incallui/video/impl/res/layout/videocall_controls_land.xml115
-rw-r--r--java/com/android/incallui/video/impl/res/values-h580dp/dimens.xml7
-rw-r--r--java/com/android/incallui/video/impl/res/values-w460dp/dimens.xml7
-rw-r--r--java/com/android/incallui/video/impl/res/values/attrs.xml8
-rw-r--r--java/com/android/incallui/video/impl/res/values/dimens.xml10
-rw-r--r--java/com/android/incallui/video/impl/res/values/strings.xml28
-rw-r--r--java/com/android/incallui/video/impl/res/values/styles.xml11
-rw-r--r--java/com/android/incallui/video/protocol/VideoCallScreen.java36
-rw-r--r--java/com/android/incallui/video/protocol/VideoCallScreenDelegate.java48
-rw-r--r--java/com/android/incallui/video/protocol/VideoCallScreenDelegateFactory.java23
-rw-r--r--java/com/android/incallui/videosurface/bindings/VideoSurfaceBindings.java44
-rw-r--r--java/com/android/incallui/videosurface/impl/VideoScale.java147
-rw-r--r--java/com/android/incallui/videosurface/impl/VideoSurfaceTextureImpl.java249
-rw-r--r--java/com/android/incallui/videosurface/protocol/VideoSurfaceDelegate.java29
-rw-r--r--java/com/android/incallui/videosurface/protocol/VideoSurfaceTexture.java57
-rw-r--r--java/com/android/incallui/wifi/AndroidManifest.xml3
-rw-r--r--java/com/android/incallui/wifi/EnableWifiCallingPrompt.java82
-rw-r--r--java/com/android/incallui/wifi/res/values/strings.xml9
476 files changed, 39486 insertions, 0 deletions
diff --git a/java/com/android/incallui/AccelerometerListener.java b/java/com/android/incallui/AccelerometerListener.java
new file mode 100644
index 000000000..01f884354
--- /dev/null
+++ b/java/com/android/incallui/AccelerometerListener.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+/**
+ * This class is used to listen to the accelerometer to monitor the orientation of the phone. The
+ * client of this class is notified when the orientation changes between horizontal and vertical.
+ */
+public class AccelerometerListener {
+
+ // Device orientation
+ public static final int ORIENTATION_UNKNOWN = 0;
+ public static final int ORIENTATION_VERTICAL = 1;
+ public static final int ORIENTATION_HORIZONTAL = 2;
+ private static final String TAG = "AccelerometerListener";
+ private static final boolean DEBUG = true;
+ private static final boolean VDEBUG = false;
+ private static final int ORIENTATION_CHANGED = 1234;
+ private static final int VERTICAL_DEBOUNCE = 100;
+ private static final int HORIZONTAL_DEBOUNCE = 500;
+ private static final double VERTICAL_ANGLE = 50.0;
+ private SensorManager mSensorManager;
+ private Sensor mSensor;
+ // mOrientation is the orientation value most recently reported to the client.
+ private int mOrientation;
+ // mPendingOrientation is the latest orientation computed based on the sensor value.
+ // This is sent to the client after a rebounce delay, at which point it is copied to
+ // mOrientation.
+ private int mPendingOrientation;
+ private OrientationListener mListener;
+ Handler mHandler =
+ new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case ORIENTATION_CHANGED:
+ synchronized (this) {
+ mOrientation = mPendingOrientation;
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "orientation: "
+ + (mOrientation == ORIENTATION_HORIZONTAL
+ ? "horizontal"
+ : (mOrientation == ORIENTATION_VERTICAL ? "vertical" : "unknown")));
+ }
+ if (mListener != null) {
+ mListener.orientationChanged(mOrientation);
+ }
+ }
+ break;
+ }
+ }
+ };
+ SensorEventListener mSensorListener =
+ new SensorEventListener() {
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ onSensorEvent(event.values[0], event.values[1], event.values[2]);
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ // ignore
+ }
+ };
+
+ public AccelerometerListener(Context context) {
+ mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
+ mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+ }
+
+ public void setListener(OrientationListener listener) {
+ mListener = listener;
+ }
+
+ public void enable(boolean enable) {
+ if (DEBUG) {
+ Log.d(TAG, "enable(" + enable + ")");
+ }
+ synchronized (this) {
+ if (enable) {
+ mOrientation = ORIENTATION_UNKNOWN;
+ mPendingOrientation = ORIENTATION_UNKNOWN;
+ mSensorManager.registerListener(
+ mSensorListener, mSensor, SensorManager.SENSOR_DELAY_NORMAL);
+ } else {
+ mSensorManager.unregisterListener(mSensorListener);
+ mHandler.removeMessages(ORIENTATION_CHANGED);
+ }
+ }
+ }
+
+ private void setOrientation(int orientation) {
+ synchronized (this) {
+ if (mPendingOrientation == orientation) {
+ // Pending orientation has not changed, so do nothing.
+ return;
+ }
+
+ // Cancel any pending messages.
+ // We will either start a new timer or cancel alltogether
+ // if the orientation has not changed.
+ mHandler.removeMessages(ORIENTATION_CHANGED);
+
+ if (mOrientation != orientation) {
+ // Set timer to send an event if the orientation has changed since its
+ // previously reported value.
+ mPendingOrientation = orientation;
+ final Message m = mHandler.obtainMessage(ORIENTATION_CHANGED);
+ // set delay to our debounce timeout
+ int delay = (orientation == ORIENTATION_VERTICAL ? VERTICAL_DEBOUNCE : HORIZONTAL_DEBOUNCE);
+ mHandler.sendMessageDelayed(m, delay);
+ } else {
+ // no message is pending
+ mPendingOrientation = ORIENTATION_UNKNOWN;
+ }
+ }
+ }
+
+ private void onSensorEvent(double x, double y, double z) {
+ if (VDEBUG) {
+ Log.d(TAG, "onSensorEvent(" + x + ", " + y + ", " + z + ")");
+ }
+
+ // If some values are exactly zero, then likely the sensor is not powered up yet.
+ // ignore these events to avoid false horizontal positives.
+ if (x == 0.0 || y == 0.0 || z == 0.0) {
+ return;
+ }
+
+ // magnitude of the acceleration vector projected onto XY plane
+ final double xy = Math.hypot(x, y);
+ // compute the vertical angle
+ double angle = Math.atan2(xy, z);
+ // convert to degrees
+ angle = angle * 180.0 / Math.PI;
+ final int orientation =
+ (angle > VERTICAL_ANGLE ? ORIENTATION_VERTICAL : ORIENTATION_HORIZONTAL);
+ if (VDEBUG) {
+ Log.d(TAG, "angle: " + angle + " orientation: " + orientation);
+ }
+ setOrientation(orientation);
+ }
+
+ public interface OrientationListener {
+
+ void orientationChanged(int orientation);
+ }
+}
diff --git a/java/com/android/incallui/AndroidManifest.xml b/java/com/android/incallui/AndroidManifest.xml
new file mode 100644
index 000000000..276b47a5e
--- /dev/null
+++ b/java/com/android/incallui/AndroidManifest.xml
@@ -0,0 +1,121 @@
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.incallui">
+
+ <uses-sdk
+ android:minSdkVersion="23"
+ android:targetSdkVersion="25"/>
+
+ <uses-permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE"/>
+ <!-- We use this to disable the status bar buttons of home, back and recent
+ during an incoming call. By doing so this allows us to not show the user
+ is viewing the activity in full screen alert, on a fresh system/factory
+ reset state of the app. -->
+ <uses-permission android:name="android.permission.STATUS_BAR"/>
+ <uses-permission android:name="android.permission.CAMERA"/>
+ <!-- Warning: setting the required boolean to true would prevent installation of Dialer on
+ devices which do not support a camera. -->
+ <uses-feature
+ android:name="android.hardware.camera.any"
+ android:required="false"/>
+
+ <!-- Testing location -->
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+
+ <!-- Set android:taskAffinity="com.android.incallui" for all activities to ensure proper
+ navigation. Otherwise system could bring up DialtactsActivity instead, e.g. when user unmerge a
+ call.
+ Set taskAffinity for application is not working because it will be merged and the result is
+ that all activities here still have same taskAffinity as activities under dialer. -->
+ <application>
+ <meta-data android:name="android.telephony.hide_voicemail_settings_menu"
+ android:value="true"/>
+ <activity
+ android:directBootAware="true"
+ android:excludeFromRecents="true"
+ android:exported="false"
+ android:label="@string/phoneAppLabel"
+ android:taskAffinity="com.android.incallui"
+ android:launchMode="singleInstance"
+ android:name="com.android.incallui.InCallActivity"
+ android:resizeableActivity="true"
+ android:screenOrientation="nosensor"
+ android:theme="@style/Theme.InCallScreen">
+ </activity>
+
+ <activity
+ android:directBootAware="true"
+ android:excludeFromRecents="true"
+ android:noHistory="true"
+ android:exported="false"
+ android:label="@string/manageConferenceLabel"
+ android:taskAffinity="com.android.incallui"
+ android:launchMode="singleTask"
+ android:name="com.android.incallui.ManageConferenceActivity"
+ android:resizeableActivity="true"
+ android:theme="@style/Theme.InCallScreen.ManageConference"/>
+
+ <service
+ android:directBootAware="true"
+ android:exported="true"
+ android:name="com.android.incallui.InCallServiceImpl"
+ android:permission="android.permission.BIND_INCALL_SERVICE">
+ <meta-data
+ android:name="android.telecom.IN_CALL_SERVICE_UI"
+ android:value="true"/>
+ <meta-data
+ android:name="android.telecom.IN_CALL_SERVICE_RINGING"
+ android:value="false"/>
+ <meta-data
+ android:name="android.telecom.INCLUDE_EXTERNAL_CALLS"
+ android:value="true"/>
+
+ <intent-filter>
+ <action android:name="android.telecom.InCallService"/>
+ </intent-filter>
+ </service>
+
+ <!--
+ Comments for attributes in SpamNotificationActivity:
+ taskAffinity="" -> Open the dialog without opening the dialer app behind it
+ noHistory="true" -> Navigating away finishes activity
+ excludeFromRecents="true" -> Don't show in "recent apps" screen
+ -->
+ <activity
+ android:excludeFromRecents="true"
+ android:exported="false"
+ android:name="com.android.incallui.spam.SpamNotificationActivity"
+ android:noHistory="true"
+ android:taskAffinity=""
+ android:theme="@style/AfterCallNotificationTheme">
+ </activity>
+
+ <service
+ android:exported="false"
+ android:name="com.android.incallui.spam.SpamNotificationService"/>
+
+ <!-- BroadcastReceiver for receiving Intents from Notification mechanism. -->
+ <receiver
+ android:directBootAware="true"
+ android:exported="false"
+ android:name="com.android.incallui.NotificationBroadcastReceiver"/>
+
+ </application>
+
+</manifest>
+
diff --git a/java/com/android/incallui/AnswerScreenPresenter.java b/java/com/android/incallui/AnswerScreenPresenter.java
new file mode 100644
index 000000000..a21876b2b
--- /dev/null
+++ b/java/com/android/incallui/AnswerScreenPresenter.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui;
+
+import android.content.Context;
+import android.support.annotation.FloatRange;
+import android.support.annotation.NonNull;
+import android.support.v4.os.UserManagerCompat;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.answer.protocol.AnswerScreen;
+import com.android.incallui.answer.protocol.AnswerScreenDelegate;
+import com.android.incallui.answerproximitysensor.AnswerProximitySensor;
+import com.android.incallui.answerproximitysensor.PseudoScreenState;
+import com.android.incallui.call.DialerCall;
+
+/** Manages changes for an incoming call screen. */
+public class AnswerScreenPresenter
+ implements AnswerScreenDelegate, DialerCall.CannedTextResponsesLoadedListener {
+ @NonNull private final Context context;
+ @NonNull private final AnswerScreen answerScreen;
+ @NonNull private final DialerCall call;
+
+ public AnswerScreenPresenter(
+ @NonNull Context context, @NonNull AnswerScreen answerScreen, @NonNull DialerCall call) {
+ LogUtil.i("AnswerScreenPresenter.constructor", null);
+ this.context = Assert.isNotNull(context);
+ this.answerScreen = Assert.isNotNull(answerScreen);
+ this.call = Assert.isNotNull(call);
+ if (isSmsResponseAllowed(call)) {
+ answerScreen.setTextResponses(call.getCannedSmsResponses());
+ }
+ call.addCannedTextResponsesLoadedListener(this);
+
+ PseudoScreenState pseudoScreenState = InCallPresenter.getInstance().getPseudoScreenState();
+ if (AnswerProximitySensor.shouldUse(context, call)) {
+ new AnswerProximitySensor(context, call, pseudoScreenState);
+ } else {
+ pseudoScreenState.setOn(true);
+ }
+ }
+
+ @Override
+ public void onAnswerScreenUnready() {
+ call.removeCannedTextResponsesLoadedListener(this);
+ }
+
+ @Override
+ public void onDismissDialog() {
+ InCallPresenter.getInstance().onDismissDialog();
+ }
+
+ @Override
+ public void onRejectCallWithMessage(String message) {
+ call.reject(true /* rejectWithMessage */, message);
+ onDismissDialog();
+ }
+
+ @Override
+ public void onAnswer(int videoState) {
+ if (answerScreen.isVideoUpgradeRequest()) {
+ call.acceptUpgradeRequest(videoState);
+ } else {
+ call.answer(videoState);
+ }
+ }
+
+ @Override
+ public void onReject() {
+ if (answerScreen.isVideoUpgradeRequest()) {
+ call.declineUpgradeRequest();
+ } else {
+ call.reject(false /* rejectWithMessage */, null);
+ }
+ }
+
+ @Override
+ public void onCannedTextResponsesLoaded(DialerCall call) {
+ if (isSmsResponseAllowed(call)) {
+ answerScreen.setTextResponses(call.getCannedSmsResponses());
+ }
+ }
+
+ @Override
+ public void updateWindowBackgroundColor(@FloatRange(from = -1f, to = 1.0f) float progress) {
+ InCallActivity activity = (InCallActivity) answerScreen.getAnswerScreenFragment().getActivity();
+ if (activity != null) {
+ activity.updateWindowBackgroundColor(progress);
+ }
+ }
+
+ private boolean isSmsResponseAllowed(DialerCall call) {
+ return UserManagerCompat.isUserUnlocked(context)
+ && call.can(android.telecom.Call.Details.CAPABILITY_RESPOND_VIA_TEXT);
+ }
+}
diff --git a/java/com/android/incallui/AnswerScreenPresenterStub.java b/java/com/android/incallui/AnswerScreenPresenterStub.java
new file mode 100644
index 000000000..fc47bf5b0
--- /dev/null
+++ b/java/com/android/incallui/AnswerScreenPresenterStub.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui;
+
+import android.support.annotation.FloatRange;
+import com.android.incallui.answer.protocol.AnswerScreenDelegate;
+
+/**
+ * Stub implementation of the answer screen delegate. Used to keep the answer fragment visible when
+ * no call exists.
+ */
+public class AnswerScreenPresenterStub implements AnswerScreenDelegate {
+ @Override
+ public void onAnswerScreenUnready() {}
+
+ @Override
+ public void onDismissDialog() {}
+
+ @Override
+ public void onRejectCallWithMessage(String message) {}
+
+ @Override
+ public void onAnswer(int videoState) {}
+
+ @Override
+ public void onReject() {}
+
+ @Override
+ public void updateWindowBackgroundColor(@FloatRange(from = -1f, to = 1.0f) float progress) {}
+}
diff --git a/java/com/android/incallui/AudioModeProvider.java b/java/com/android/incallui/AudioModeProvider.java
new file mode 100644
index 000000000..698db0ab9
--- /dev/null
+++ b/java/com/android/incallui/AudioModeProvider.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui;
+
+import android.telecom.CallAudioState;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Proxy class for getting and setting the audio mode. */
+public class AudioModeProvider {
+ private static final int SUPPORTED_AUDIO_ROUTE_ALL =
+ CallAudioState.ROUTE_EARPIECE
+ | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_WIRED_HEADSET
+ | CallAudioState.ROUTE_SPEAKER;
+
+ private static final AudioModeProvider instance = new AudioModeProvider();
+ private final List<AudioModeListener> listeners = new ArrayList<>();
+ private CallAudioState audioState =
+ new CallAudioState(false, CallAudioState.ROUTE_EARPIECE, SUPPORTED_AUDIO_ROUTE_ALL);
+
+ public static AudioModeProvider getInstance() {
+ return instance;
+ }
+
+ public void onAudioStateChanged(CallAudioState audioState) {
+ if (!this.audioState.equals(audioState)) {
+ this.audioState = audioState;
+ for (AudioModeListener listener : listeners) {
+ listener.onAudioStateChanged(audioState);
+ }
+ }
+ }
+
+ public void addListener(AudioModeListener listener) {
+ if (!listeners.contains(listener)) {
+ listeners.add(listener);
+ listener.onAudioStateChanged(audioState);
+ }
+ }
+
+ public void removeListener(AudioModeListener listener) {
+ listeners.remove(listener);
+ }
+
+ public CallAudioState getAudioState() {
+ return audioState;
+ }
+
+ /** Notified on changes to audio mode. */
+ public interface AudioModeListener {
+
+ void onAudioStateChanged(CallAudioState audioState);
+ }
+}
diff --git a/java/com/android/incallui/Bindings.java b/java/com/android/incallui/Bindings.java
new file mode 100644
index 000000000..4f142ff96
--- /dev/null
+++ b/java/com/android/incallui/Bindings.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui;
+
+import android.content.Context;
+import com.android.incallui.bindings.InCallUiBindings;
+import com.android.incallui.bindings.InCallUiBindingsFactory;
+import com.android.incallui.bindings.InCallUiBindingsStub;
+import java.util.Objects;
+
+/** Accessor for the in call UI bindings. */
+public class Bindings {
+
+ private static InCallUiBindings instance;
+
+ private Bindings() {}
+
+ public static InCallUiBindings get(Context context) {
+ Objects.requireNonNull(context);
+ if (instance != null) {
+ return instance;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof InCallUiBindingsFactory) {
+ instance = ((InCallUiBindingsFactory) application).newInCallUiBindings();
+ }
+
+ if (instance == null) {
+ instance = new InCallUiBindingsStub();
+ }
+ return instance;
+ }
+
+ public static void setForTesting(InCallUiBindings testInstance) {
+ instance = testInstance;
+ }
+}
diff --git a/java/com/android/incallui/CallButtonPresenter.java b/java/com/android/incallui/CallButtonPresenter.java
new file mode 100644
index 000000000..d6f4cddc9
--- /dev/null
+++ b/java/com/android/incallui/CallButtonPresenter.java
@@ -0,0 +1,515 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.os.UserManagerCompat;
+import android.telecom.CallAudioState;
+import android.telecom.InCallService.VideoCall;
+import android.telecom.VideoProfile;
+import com.android.contacts.common.compat.CallCompat;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.SdkVersionOverride;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.incallui.AudioModeProvider.AudioModeListener;
+import com.android.incallui.InCallCameraManager.Listener;
+import com.android.incallui.InCallPresenter.CanAddCallListener;
+import com.android.incallui.InCallPresenter.InCallDetailsListener;
+import com.android.incallui.InCallPresenter.InCallState;
+import com.android.incallui.InCallPresenter.InCallStateListener;
+import com.android.incallui.InCallPresenter.IncomingCallListener;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.TelecomAdapter;
+import com.android.incallui.call.VideoUtils;
+import com.android.incallui.incall.protocol.InCallButtonIds;
+import com.android.incallui.incall.protocol.InCallButtonUi;
+import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
+
+/** Logic for call buttons. */
+public class CallButtonPresenter
+ implements InCallStateListener,
+ AudioModeListener,
+ IncomingCallListener,
+ InCallDetailsListener,
+ CanAddCallListener,
+ Listener,
+ InCallButtonUiDelegate {
+
+ private static final String KEY_AUTOMATICALLY_MUTED = "incall_key_automatically_muted";
+ private static final String KEY_PREVIOUS_MUTE_STATE = "incall_key_previous_mute_state";
+
+ private final Context mContext;
+ private InCallButtonUi mInCallButtonUi;
+ private DialerCall mCall;
+ private boolean mAutomaticallyMuted = false;
+ private boolean mPreviousMuteState = false;
+ private boolean isInCallButtonUiReady;
+
+ public CallButtonPresenter(Context context) {
+ mContext = context.getApplicationContext();
+ }
+
+ @Override
+ public void onInCallButtonUiReady(InCallButtonUi ui) {
+ Assert.checkState(!isInCallButtonUiReady);
+ mInCallButtonUi = ui;
+ AudioModeProvider.getInstance().addListener(this);
+
+ // register for call state changes last
+ final InCallPresenter inCallPresenter = InCallPresenter.getInstance();
+ inCallPresenter.addListener(this);
+ inCallPresenter.addIncomingCallListener(this);
+ inCallPresenter.addDetailsListener(this);
+ inCallPresenter.addCanAddCallListener(this);
+ inCallPresenter.getInCallCameraManager().addCameraSelectionListener(this);
+
+ // Update the buttons state immediately for the current call
+ onStateChange(InCallState.NO_CALLS, inCallPresenter.getInCallState(), CallList.getInstance());
+ isInCallButtonUiReady = true;
+ }
+
+ @Override
+ public void onInCallButtonUiUnready() {
+ Assert.checkState(isInCallButtonUiReady);
+ mInCallButtonUi = null;
+ InCallPresenter.getInstance().removeListener(this);
+ AudioModeProvider.getInstance().removeListener(this);
+ InCallPresenter.getInstance().removeIncomingCallListener(this);
+ InCallPresenter.getInstance().removeDetailsListener(this);
+ InCallPresenter.getInstance().getInCallCameraManager().removeCameraSelectionListener(this);
+ InCallPresenter.getInstance().removeCanAddCallListener(this);
+ isInCallButtonUiReady = false;
+ }
+
+ @Override
+ public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
+ if (newState == InCallState.OUTGOING) {
+ mCall = callList.getOutgoingCall();
+ } else if (newState == InCallState.INCALL) {
+ mCall = callList.getActiveOrBackgroundCall();
+
+ // When connected to voice mail, automatically shows the dialpad.
+ // (On previous releases we showed it when in-call shows up, before waiting for
+ // OUTGOING. We may want to do that once we start showing "Voice mail" label on
+ // the dialpad too.)
+ if (oldState == InCallState.OUTGOING && mCall != null) {
+ if (CallerInfoUtils.isVoiceMailNumber(mContext, mCall) && getActivity() != null) {
+ getActivity().showDialpadFragment(true /* show */, true /* animate */);
+ }
+ }
+ } else if (newState == InCallState.INCOMING) {
+ if (getActivity() != null) {
+ getActivity().showDialpadFragment(false /* show */, true /* animate */);
+ }
+ mCall = callList.getIncomingCall();
+ } else {
+ mCall = null;
+ }
+ updateUi(newState, mCall);
+ }
+
+ /**
+ * Updates the user interface in response to a change in the details of a call. Currently handles
+ * changes to the call buttons in response to a change in the details for a call. This is
+ * important to ensure changes to the active call are reflected in the available buttons.
+ *
+ * @param call The active call.
+ * @param details The call details.
+ */
+ @Override
+ public void onDetailsChanged(DialerCall call, android.telecom.Call.Details details) {
+ // Only update if the changes are for the currently active call
+ if (mInCallButtonUi != null && call != null && call.equals(mCall)) {
+ updateButtonsState(call);
+ }
+ }
+
+ @Override
+ public void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call) {
+ onStateChange(oldState, newState, CallList.getInstance());
+ }
+
+ @Override
+ public void onCanAddCallChanged(boolean canAddCall) {
+ if (mInCallButtonUi != null && mCall != null) {
+ updateButtonsState(mCall);
+ }
+ }
+
+ @Override
+ public void onAudioStateChanged(CallAudioState audioState) {
+ if (mInCallButtonUi != null) {
+ mInCallButtonUi.setAudioState(audioState);
+ }
+ }
+
+ @Override
+ public CallAudioState getCurrentAudioState() {
+ return AudioModeProvider.getInstance().getAudioState();
+ }
+
+ @Override
+ public void setAudioRoute(int route) {
+ LogUtil.i(
+ "CallButtonPresenter.setAudioRoute",
+ "sending new audio route: " + CallAudioState.audioRouteToString(route));
+ TelecomAdapter.getInstance().setAudioRoute(route);
+ }
+
+ /** Function assumes that bluetooth is not supported. */
+ @Override
+ public void toggleSpeakerphone() {
+ // This function should not be called if bluetooth is available.
+ CallAudioState audioState = getCurrentAudioState();
+ if (0 != (CallAudioState.ROUTE_BLUETOOTH & audioState.getSupportedRouteMask())) {
+ // It's clear the UI is wrong, so update the supported mode once again.
+ LogUtil.e(
+ "CallButtonPresenter", "toggling speakerphone not allowed when bluetooth supported.");
+ mInCallButtonUi.setAudioState(audioState);
+ return;
+ }
+
+ int newRoute;
+ if (audioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {
+ newRoute = CallAudioState.ROUTE_WIRED_OR_EARPIECE;
+ Logger.get(mContext)
+ .logCallImpression(
+ DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_WIRED_OR_EARPIECE,
+ mCall.getUniqueCallId(),
+ mCall.getTimeAddedMs());
+ } else {
+ newRoute = CallAudioState.ROUTE_SPEAKER;
+ Logger.get(mContext)
+ .logCallImpression(
+ DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_SPEAKERPHONE,
+ mCall.getUniqueCallId(),
+ mCall.getTimeAddedMs());
+ }
+
+ setAudioRoute(newRoute);
+ }
+
+ @Override
+ public void muteClicked(boolean checked) {
+ LogUtil.v("CallButtonPresenter", "turning on mute: " + checked);
+ TelecomAdapter.getInstance().mute(checked);
+ }
+
+ @Override
+ public void holdClicked(boolean checked) {
+ if (mCall == null) {
+ return;
+ }
+ if (checked) {
+ LogUtil.i("CallButtonPresenter", "putting the call on hold: " + mCall);
+ mCall.hold();
+ } else {
+ LogUtil.i("CallButtonPresenter", "removing the call from hold: " + mCall);
+ mCall.unhold();
+ }
+ }
+
+ @Override
+ public void swapClicked() {
+ if (mCall == null) {
+ return;
+ }
+
+ LogUtil.i("CallButtonPresenter", "swapping the call: " + mCall);
+ TelecomAdapter.getInstance().swap(mCall.getId());
+ }
+
+ @Override
+ public void mergeClicked() {
+ TelecomAdapter.getInstance().merge(mCall.getId());
+ }
+
+ @Override
+ public void addCallClicked() {
+ // Automatically mute the current call
+ mAutomaticallyMuted = true;
+ mPreviousMuteState = AudioModeProvider.getInstance().getAudioState().isMuted();
+ // Simulate a click on the mute button
+ muteClicked(true);
+ TelecomAdapter.getInstance().addCall();
+ }
+
+ @Override
+ public void showDialpadClicked(boolean checked) {
+ LogUtil.v("CallButtonPresenter", "show dialpad " + String.valueOf(checked));
+ getActivity().showDialpadFragment(checked /* show */, true /* animate */);
+ }
+
+ @Override
+ public void changeToVideoClicked() {
+ VideoCall videoCall = mCall.getVideoCall();
+ if (videoCall == null) {
+ return;
+ }
+ int currVideoState = mCall.getVideoState();
+ int currUnpausedVideoState = VideoUtils.getUnPausedVideoState(currVideoState);
+ currUnpausedVideoState |= VideoProfile.STATE_BIDIRECTIONAL;
+
+ VideoProfile videoProfile = new VideoProfile(currUnpausedVideoState);
+ videoCall.sendSessionModifyRequest(videoProfile);
+ mCall.setSessionModificationState(
+ DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE);
+ }
+
+ @Override
+ public void onEndCallClicked() {
+ LogUtil.i("CallButtonPresenter.onEndCallClicked", "call: " + mCall);
+ if (mCall != null) {
+ mCall.disconnect();
+ }
+ }
+
+ @Override
+ public void showAudioRouteSelector() {
+ mInCallButtonUi.showAudioRouteSelector();
+ }
+
+ /**
+ * Switches the camera between the front-facing and back-facing camera.
+ *
+ * @param useFrontFacingCamera True if we should switch to using the front-facing camera, or false
+ * if we should switch to using the back-facing camera.
+ */
+ @Override
+ public void switchCameraClicked(boolean useFrontFacingCamera) {
+ InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager();
+ cameraManager.setUseFrontFacingCamera(useFrontFacingCamera);
+
+ VideoCall videoCall = mCall.getVideoCall();
+ if (videoCall == null) {
+ return;
+ }
+
+ String cameraId = cameraManager.getActiveCameraId();
+ if (cameraId != null) {
+ final int cameraDir =
+ cameraManager.isUsingFrontFacingCamera()
+ ? DialerCall.VideoSettings.CAMERA_DIRECTION_FRONT_FACING
+ : DialerCall.VideoSettings.CAMERA_DIRECTION_BACK_FACING;
+ mCall.getVideoSettings().setCameraDir(cameraDir);
+ videoCall.setCamera(cameraId);
+ videoCall.requestCameraCapabilities();
+ }
+ }
+
+ @Override
+ public void toggleCameraClicked() {
+ LogUtil.i("CallButtonPresenter.toggleCameraClicked", "");
+ switchCameraClicked(
+ !InCallPresenter.getInstance().getInCallCameraManager().isUsingFrontFacingCamera());
+ }
+
+ /**
+ * Stop or start client's video transmission.
+ *
+ * @param pause True if pausing the local user's video, or false if starting the local user's
+ * video.
+ */
+ @Override
+ public void pauseVideoClicked(boolean pause) {
+ LogUtil.i("CallButtonPresenter.pauseVideoClicked", "%s", pause ? "pause" : "unpause");
+ VideoCall videoCall = mCall.getVideoCall();
+ if (videoCall == null) {
+ return;
+ }
+
+ int currUnpausedVideoState = VideoUtils.getUnPausedVideoState(mCall.getVideoState());
+ if (pause) {
+ videoCall.setCamera(null);
+ VideoProfile videoProfile =
+ new VideoProfile(currUnpausedVideoState & ~VideoProfile.STATE_TX_ENABLED);
+ videoCall.sendSessionModifyRequest(videoProfile);
+ } else {
+ InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager();
+ videoCall.setCamera(cameraManager.getActiveCameraId());
+ VideoProfile videoProfile =
+ new VideoProfile(currUnpausedVideoState | VideoProfile.STATE_TX_ENABLED);
+ videoCall.sendSessionModifyRequest(videoProfile);
+ mCall.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE);
+ }
+
+ mInCallButtonUi.setVideoPaused(pause);
+ mInCallButtonUi.enableButton(InCallButtonIds.BUTTON_PAUSE_VIDEO, false);
+ }
+
+ private void updateUi(InCallState state, DialerCall call) {
+ LogUtil.v("CallButtonPresenter", "updating call UI for call: ", call);
+
+ if (mInCallButtonUi == null) {
+ return;
+ }
+
+ if (call != null) {
+ mInCallButtonUi.updateInCallButtonUiColors();
+ }
+
+ final boolean isEnabled =
+ state.isConnectingOrConnected() && !state.isIncoming() && call != null;
+ mInCallButtonUi.setEnabled(isEnabled);
+
+ if (call == null) {
+ return;
+ }
+
+ updateButtonsState(call);
+ }
+
+ /**
+ * Updates the buttons applicable for the UI.
+ *
+ * @param call The active call.
+ */
+ private void updateButtonsState(DialerCall call) {
+ LogUtil.v("CallButtonPresenter.updateButtonsState", "");
+ final boolean isVideo = VideoUtils.isVideoCall(call);
+
+ // Common functionality (audio, hold, etc).
+ // Show either HOLD or SWAP, but not both. If neither HOLD or SWAP is available:
+ // (1) If the device normally can hold, show HOLD in a disabled state.
+ // (2) If the device doesn't have the concept of hold/swap, remove the button.
+ final boolean showSwap = call.can(android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE);
+ final boolean showHold =
+ !showSwap
+ && call.can(android.telecom.Call.Details.CAPABILITY_SUPPORT_HOLD)
+ && call.can(android.telecom.Call.Details.CAPABILITY_HOLD);
+ final boolean isCallOnHold = call.getState() == DialerCall.State.ONHOLD;
+
+ final boolean showAddCall =
+ TelecomAdapter.getInstance().canAddCall() && UserManagerCompat.isUserUnlocked(mContext);
+ final boolean showMerge = call.can(android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE);
+ final boolean showUpgradeToVideo = !isVideo && hasVideoCallCapabilities(call);
+ final boolean showDowngradeToAudio = isVideo && isDowngradeToAudioSupported(call);
+ final boolean showMute = call.can(android.telecom.Call.Details.CAPABILITY_MUTE);
+
+ final boolean hasCameraPermission =
+ isVideo && VideoUtils.hasCameraPermissionAndAllowedByUser(mContext);
+ // Disabling local video doesn't seem to work when dialing. See b/30256571.
+ final boolean showPauseVideo =
+ isVideo
+ && call.getState() != DialerCall.State.DIALING
+ && call.getState() != DialerCall.State.CONNECTING;
+
+ mInCallButtonUi.showButton(InCallButtonIds.BUTTON_AUDIO, true);
+ mInCallButtonUi.showButton(InCallButtonIds.BUTTON_SWAP, showSwap);
+ mInCallButtonUi.showButton(InCallButtonIds.BUTTON_HOLD, showHold);
+ mInCallButtonUi.setHold(isCallOnHold);
+ mInCallButtonUi.showButton(InCallButtonIds.BUTTON_MUTE, showMute);
+ mInCallButtonUi.showButton(InCallButtonIds.BUTTON_ADD_CALL, true);
+ mInCallButtonUi.enableButton(InCallButtonIds.BUTTON_ADD_CALL, showAddCall);
+ mInCallButtonUi.showButton(InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO, showUpgradeToVideo);
+ mInCallButtonUi.showButton(InCallButtonIds.BUTTON_DOWNGRADE_TO_AUDIO, showDowngradeToAudio);
+ mInCallButtonUi.showButton(
+ InCallButtonIds.BUTTON_SWITCH_CAMERA, isVideo && hasCameraPermission);
+ mInCallButtonUi.showButton(InCallButtonIds.BUTTON_PAUSE_VIDEO, showPauseVideo);
+ if (isVideo) {
+ mInCallButtonUi.setVideoPaused(
+ !VideoUtils.isTransmissionEnabled(call) || !hasCameraPermission);
+ }
+ mInCallButtonUi.showButton(InCallButtonIds.BUTTON_DIALPAD, true);
+ mInCallButtonUi.showButton(InCallButtonIds.BUTTON_MERGE, showMerge);
+
+ mInCallButtonUi.updateButtonStates();
+ }
+
+ private boolean hasVideoCallCapabilities(DialerCall call) {
+ if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M) {
+ return call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX)
+ && call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_RX);
+ }
+ // In L, this single flag represents both video transmitting and receiving capabilities
+ return call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX);
+ }
+
+ /**
+ * Determines if downgrading from a video call to an audio-only call is supported. In order to
+ * support downgrade to audio, the SDK version must be >= N and the call should NOT have the
+ * {@link android.telecom.Call.Details#CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO}.
+ *
+ * @param call The call.
+ * @return {@code true} if downgrading to an audio-only call from a video call is supported.
+ */
+ private boolean isDowngradeToAudioSupported(DialerCall call) {
+ return !call.can(CallCompat.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO);
+ }
+
+ @Override
+ public void refreshMuteState() {
+ // Restore the previous mute state
+ if (mAutomaticallyMuted
+ && AudioModeProvider.getInstance().getAudioState().isMuted() != mPreviousMuteState) {
+ if (mInCallButtonUi == null) {
+ return;
+ }
+ muteClicked(mPreviousMuteState);
+ }
+ mAutomaticallyMuted = false;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putBoolean(KEY_AUTOMATICALLY_MUTED, mAutomaticallyMuted);
+ outState.putBoolean(KEY_PREVIOUS_MUTE_STATE, mPreviousMuteState);
+ }
+
+ @Override
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ mAutomaticallyMuted =
+ savedInstanceState.getBoolean(KEY_AUTOMATICALLY_MUTED, mAutomaticallyMuted);
+ mPreviousMuteState = savedInstanceState.getBoolean(KEY_PREVIOUS_MUTE_STATE, mPreviousMuteState);
+ }
+
+ @Override
+ public void onCameraPermissionGranted() {
+ if (mCall != null) {
+ updateButtonsState(mCall);
+ }
+ }
+
+ @Override
+ public void onActiveCameraSelectionChanged(boolean isUsingFrontFacingCamera) {
+ if (mInCallButtonUi == null) {
+ return;
+ }
+ mInCallButtonUi.setCameraSwitched(!isUsingFrontFacingCamera);
+ }
+
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+ private InCallActivity getActivity() {
+ if (mInCallButtonUi != null) {
+ Fragment fragment = mInCallButtonUi.getInCallButtonUiFragment();
+ if (fragment != null) {
+ return (InCallActivity) fragment.getActivity();
+ }
+ }
+ return null;
+ }
+}
diff --git a/java/com/android/incallui/CallCardPresenter.java b/java/com/android/incallui/CallCardPresenter.java
new file mode 100644
index 000000000..930775772
--- /dev/null
+++ b/java/com/android/incallui/CallCardPresenter.java
@@ -0,0 +1,1110 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui;
+
+import static com.android.contacts.common.compat.CallCompat.Details.PROPERTY_ENTERPRISE_CALL;
+
+import android.Manifest;
+import android.app.Application;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.hardware.display.DisplayManager;
+import android.os.BatteryManager;
+import android.os.Handler;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.support.v4.content.ContextCompat;
+import android.telecom.Call.Details;
+import android.telecom.StatusHints;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import android.view.Display;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.enrichedcall.EnrichedCallManager;
+import com.android.dialer.enrichedcall.Session;
+import com.android.dialer.multimedia.MultimediaData;
+import com.android.incallui.ContactInfoCache.ContactCacheEntry;
+import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
+import com.android.incallui.InCallPresenter.InCallDetailsListener;
+import com.android.incallui.InCallPresenter.InCallEventListener;
+import com.android.incallui.InCallPresenter.InCallState;
+import com.android.incallui.InCallPresenter.InCallStateListener;
+import com.android.incallui.InCallPresenter.IncomingCallListener;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.call.DialerCallListener;
+import com.android.incallui.incall.protocol.ContactPhotoType;
+import com.android.incallui.incall.protocol.InCallScreen;
+import com.android.incallui.incall.protocol.InCallScreenDelegate;
+import com.android.incallui.incall.protocol.PrimaryCallState;
+import com.android.incallui.incall.protocol.PrimaryInfo;
+import com.android.incallui.incall.protocol.SecondaryInfo;
+import java.lang.ref.WeakReference;
+
+/**
+ * Controller for the Call Card Fragment. This class listens for changes to InCallState and passes
+ * it along to the fragment.
+ */
+public class CallCardPresenter
+ implements InCallStateListener,
+ IncomingCallListener,
+ InCallDetailsListener,
+ InCallEventListener,
+ InCallScreenDelegate,
+ DialerCallListener,
+ EnrichedCallManager.StateChangedListener {
+
+ /**
+ * Amount of time to wait before sending an announcement via the accessibility manager. When the
+ * call state changes to an outgoing or incoming state for the first time, the UI can often be
+ * changing due to call updates or contact lookup. This allows the UI to settle to a stable state
+ * to ensure that the correct information is announced.
+ */
+ private static final long ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS = 500;
+
+ /** Flag to allow the user's current location to be shown during emergency calls. */
+ private static final String CONFIG_ENABLE_EMERGENCY_LOCATION = "config_enable_emergency_location";
+
+ private static final boolean CONFIG_ENABLE_EMERGENCY_LOCATION_DEFAULT = true;
+
+ /**
+ * Make it possible to not get location during an emergency call if the battery is too low, since
+ * doing so could trigger gps and thus potentially cause the phone to die in the middle of the
+ * call.
+ */
+ private static final String CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION =
+ "min_battery_percent_for_emergency_location";
+
+ private static final long CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION_DEFAULT = 10;
+
+ private final Context mContext;
+ private final Handler handler = new Handler();
+
+ private DialerCall mPrimary;
+ private DialerCall mSecondary;
+ private ContactCacheEntry mPrimaryContactInfo;
+ private ContactCacheEntry mSecondaryContactInfo;
+ @Nullable private ContactsPreferences mContactsPreferences;
+ private boolean mIsFullscreen = false;
+ private InCallScreen mInCallScreen;
+ private boolean isInCallScreenReady;
+ private boolean shouldSendAccessibilityEvent;
+ private final String locationModule = null;
+ private final Runnable sendAccessibilityEventRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ shouldSendAccessibilityEvent = !sendAccessibilityEvent(mContext, getUi());
+ LogUtil.i(
+ "CallCardPresenter.sendAccessibilityEventRunnable",
+ "still should send: %b",
+ shouldSendAccessibilityEvent);
+ if (!shouldSendAccessibilityEvent) {
+ handler.removeCallbacks(this);
+ }
+ }
+ };
+
+ public CallCardPresenter(Context context) {
+ LogUtil.i("CallCardController.constructor", null);
+ mContext = Assert.isNotNull(context).getApplicationContext();
+ }
+
+ private static boolean hasCallSubject(DialerCall call) {
+ return !TextUtils.isEmpty(call.getCallSubject());
+ }
+
+ @Override
+ public void onInCallScreenDelegateInit(InCallScreen inCallScreen) {
+ Assert.isNotNull(inCallScreen);
+ mInCallScreen = inCallScreen;
+ mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext);
+
+ // Call may be null if disconnect happened already.
+ DialerCall call = CallList.getInstance().getFirstCall();
+ if (call != null) {
+ mPrimary = call;
+ if (shouldShowNoteSentToast(mPrimary)) {
+ mInCallScreen.showNoteSentToast();
+ }
+ call.addListener(this);
+
+ // start processing lookups right away.
+ if (!call.isConferenceCall()) {
+ startContactInfoSearch(call, true, call.getState() == DialerCall.State.INCOMING);
+ } else {
+ updateContactEntry(null, true);
+ }
+ }
+
+ onStateChange(null, InCallPresenter.getInstance().getInCallState(), CallList.getInstance());
+ }
+
+ @Override
+ public void onInCallScreenReady() {
+ LogUtil.i("CallCardController.onInCallScreenReady", null);
+ Assert.checkState(!isInCallScreenReady);
+ if (mContactsPreferences != null) {
+ mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
+ }
+
+ EnrichedCallManager.Accessor.getInstance(((Application) mContext))
+ .registerStateChangedListener(this);
+
+ // Contact search may have completed before ui is ready.
+ if (mPrimaryContactInfo != null) {
+ updatePrimaryDisplayInfo();
+ }
+
+ // Register for call state changes last
+ InCallPresenter.getInstance().addListener(this);
+ InCallPresenter.getInstance().addIncomingCallListener(this);
+ InCallPresenter.getInstance().addDetailsListener(this);
+ InCallPresenter.getInstance().addInCallEventListener(this);
+ isInCallScreenReady = true;
+ }
+
+ @Override
+ public void onInCallScreenUnready() {
+ LogUtil.i("CallCardController.onInCallScreenUnready", null);
+ Assert.checkState(isInCallScreenReady);
+
+ EnrichedCallManager.Accessor.getInstance(((Application) mContext))
+ .unregisterStateChangedListener(this);
+ // stop getting call state changes
+ InCallPresenter.getInstance().removeListener(this);
+ InCallPresenter.getInstance().removeIncomingCallListener(this);
+ InCallPresenter.getInstance().removeDetailsListener(this);
+ InCallPresenter.getInstance().removeInCallEventListener(this);
+ if (mPrimary != null) {
+ mPrimary.removeListener(this);
+ }
+
+ mPrimary = null;
+ mPrimaryContactInfo = null;
+ mSecondaryContactInfo = null;
+ isInCallScreenReady = false;
+ }
+
+ @Override
+ public void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call) {
+ // same logic should happen as with onStateChange()
+ onStateChange(oldState, newState, CallList.getInstance());
+ }
+
+ @Override
+ public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
+ LogUtil.v("CallCardPresenter.onStateChange", "" + newState);
+ if (mInCallScreen == null) {
+ return;
+ }
+
+ DialerCall primary = null;
+ DialerCall secondary = null;
+
+ if (newState == InCallState.INCOMING) {
+ primary = callList.getIncomingCall();
+ } else if (newState == InCallState.PENDING_OUTGOING || newState == InCallState.OUTGOING) {
+ primary = callList.getOutgoingCall();
+ if (primary == null) {
+ primary = callList.getPendingOutgoingCall();
+ }
+
+ // getCallToDisplay doesn't go through outgoing or incoming calls. It will return the
+ // highest priority call to display as the secondary call.
+ secondary = getCallToDisplay(callList, null, true);
+ } else if (newState == InCallState.INCALL) {
+ primary = getCallToDisplay(callList, null, false);
+ secondary = getCallToDisplay(callList, primary, true);
+ }
+
+ LogUtil.v("CallCardPresenter.onStateChange", "primary call: " + primary);
+ LogUtil.v("CallCardPresenter.onStateChange", "secondary call: " + secondary);
+
+ final boolean primaryChanged =
+ !(DialerCall.areSame(mPrimary, primary) && DialerCall.areSameNumber(mPrimary, primary));
+ final boolean secondaryChanged =
+ !(DialerCall.areSame(mSecondary, secondary)
+ && DialerCall.areSameNumber(mSecondary, secondary));
+
+ mSecondary = secondary;
+ DialerCall previousPrimary = mPrimary;
+ mPrimary = primary;
+
+ if (mPrimary != null) {
+ InCallPresenter.getInstance().onForegroundCallChanged(mPrimary);
+ mInCallScreen.updateInCallScreenColors();
+ }
+
+ if (primaryChanged && shouldShowNoteSentToast(primary)) {
+ mInCallScreen.showNoteSentToast();
+ }
+
+ // Refresh primary call information if either:
+ // 1. Primary call changed.
+ // 2. The call's ability to manage conference has changed.
+ if (shouldRefreshPrimaryInfo(primaryChanged)) {
+ // primary call has changed
+ if (previousPrimary != null) {
+ previousPrimary.removeListener(this);
+ }
+ mPrimary.addListener(this);
+
+ mPrimaryContactInfo =
+ ContactInfoCache.buildCacheEntryFromCall(
+ mContext, mPrimary, mPrimary.getState() == DialerCall.State.INCOMING);
+ updatePrimaryDisplayInfo();
+ maybeStartSearch(mPrimary, true);
+ maybeClearSessionModificationState(mPrimary);
+ }
+
+ if (previousPrimary != null && mPrimary == null) {
+ previousPrimary.removeListener(this);
+ }
+
+ if (mSecondary == null) {
+ // Secondary call may have ended. Update the ui.
+ mSecondaryContactInfo = null;
+ updateSecondaryDisplayInfo();
+ } else if (secondaryChanged) {
+ // secondary call has changed
+ mSecondaryContactInfo =
+ ContactInfoCache.buildCacheEntryFromCall(
+ mContext, mSecondary, mSecondary.getState() == DialerCall.State.INCOMING);
+ updateSecondaryDisplayInfo();
+ maybeStartSearch(mSecondary, false);
+ maybeClearSessionModificationState(mSecondary);
+ }
+
+ // Set the call state
+ int callState = DialerCall.State.IDLE;
+ if (mPrimary != null) {
+ callState = mPrimary.getState();
+ updatePrimaryCallState();
+ } else {
+ getUi().setCallState(PrimaryCallState.createEmptyPrimaryCallState());
+ }
+
+ maybeShowManageConferenceCallButton();
+
+ // Hide the end call button instantly if we're receiving an incoming call.
+ getUi()
+ .setEndCallButtonEnabled(
+ shouldShowEndCallButton(mPrimary, callState),
+ callState != DialerCall.State.INCOMING /* animate */);
+
+ maybeSendAccessibilityEvent(oldState, newState, primaryChanged);
+ }
+
+ @Override
+ public void onDetailsChanged(DialerCall call, Details details) {
+ updatePrimaryCallState();
+
+ if (call.can(Details.CAPABILITY_MANAGE_CONFERENCE)
+ != details.can(Details.CAPABILITY_MANAGE_CONFERENCE)) {
+ maybeShowManageConferenceCallButton();
+ }
+ }
+
+ @Override
+ public void onDialerCallDisconnect() {}
+
+ @Override
+ public void onDialerCallUpdate() {
+ // No-op; specific call updates handled elsewhere.
+ }
+
+ @Override
+ public void onWiFiToLteHandover() {}
+
+ @Override
+ public void onHandoverToWifiFailure() {}
+
+ /** Handles a change to the child number by refreshing the primary call info. */
+ @Override
+ public void onDialerCallChildNumberChange() {
+ LogUtil.v("CallCardPresenter.onDialerCallChildNumberChange", "");
+
+ if (mPrimary == null) {
+ return;
+ }
+ updatePrimaryDisplayInfo();
+ }
+
+ /** Handles a change to the last forwarding number by refreshing the primary call info. */
+ @Override
+ public void onDialerCallLastForwardedNumberChange() {
+ LogUtil.v("CallCardPresenter.onDialerCallLastForwardedNumberChange", "");
+
+ if (mPrimary == null) {
+ return;
+ }
+ updatePrimaryDisplayInfo();
+ updatePrimaryCallState();
+ }
+
+ @Override
+ public void onDialerCallUpgradeToVideo() {}
+
+ /**
+ * Handles a change to the session modification state for a call.
+ *
+ * @param sessionModificationState The new session modification state.
+ */
+ @Override
+ public void onDialerCallSessionModificationStateChange(
+ @SessionModificationState int sessionModificationState) {
+ LogUtil.v(
+ "CallCardPresenter.onDialerCallSessionModificationStateChange",
+ "state: " + sessionModificationState);
+
+ if (mPrimary == null) {
+ return;
+ }
+ getUi()
+ .setEndCallButtonEnabled(
+ sessionModificationState
+ != DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
+ true /* shouldAnimate */);
+ updatePrimaryCallState();
+ }
+
+ @Override
+ public void onEnrichedCallStateChanged() {
+ LogUtil.enterBlock("CallCardPresenter.onEnrichedCallStateChanged");
+ updatePrimaryDisplayInfo();
+ }
+
+ private boolean shouldRefreshPrimaryInfo(boolean primaryChanged) {
+ if (mPrimary == null) {
+ return false;
+ }
+ return primaryChanged
+ || mInCallScreen.isManageConferenceVisible() != shouldShowManageConference();
+ }
+
+ private void updatePrimaryCallState() {
+ if (getUi() != null && mPrimary != null) {
+ boolean isWorkCall =
+ mPrimary.hasProperty(PROPERTY_ENTERPRISE_CALL)
+ || (mPrimaryContactInfo != null
+ && mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
+ boolean isHdAudioCall =
+ isPrimaryCallActive() && mPrimary.hasProperty(Details.PROPERTY_HIGH_DEF_AUDIO);
+ // Check for video state change and update the visibility of the contact photo. The contact
+ // photo is hidden when the incoming video surface is shown.
+ // The contact photo visibility can also change in setPrimary().
+ boolean shouldShowContactPhoto =
+ !VideoCallPresenter.showIncomingVideo(mPrimary.getVideoState(), mPrimary.getState());
+ getUi()
+ .setCallState(
+ new PrimaryCallState(
+ mPrimary.getState(),
+ mPrimary.getVideoState(),
+ mPrimary.getSessionModificationState(),
+ mPrimary.getDisconnectCause(),
+ getConnectionLabel(),
+ getCallStateIcon(),
+ getGatewayNumber(),
+ shouldShowCallSubject(mPrimary) ? mPrimary.getCallSubject() : null,
+ mPrimary.getCallbackNumber(),
+ mPrimary.hasProperty(Details.PROPERTY_WIFI),
+ mPrimary.isConferenceCall(),
+ isWorkCall,
+ isHdAudioCall,
+ !TextUtils.isEmpty(mPrimary.getLastForwardedNumber()),
+ shouldShowContactPhoto,
+ mPrimary.getConnectTimeMillis(),
+ CallerInfoUtils.isVoiceMailNumber(mContext, mPrimary),
+ mPrimary.isRemotelyHeld()));
+
+ InCallActivity activity =
+ (InCallActivity) (mInCallScreen.getInCallScreenFragment().getActivity());
+ if (activity != null) {
+ activity.onPrimaryCallStateChanged();
+ }
+ }
+ }
+
+ /** Only show the conference call button if we can manage the conference. */
+ private void maybeShowManageConferenceCallButton() {
+ getUi().showManageConferenceCallButton(shouldShowManageConference());
+ }
+
+ /**
+ * Determines if the manage conference button should be visible, based on the current primary
+ * call.
+ *
+ * @return {@code True} if the manage conference button should be visible.
+ */
+ private boolean shouldShowManageConference() {
+ if (mPrimary == null) {
+ return false;
+ }
+
+ return mPrimary.can(android.telecom.Call.Details.CAPABILITY_MANAGE_CONFERENCE)
+ && !mIsFullscreen;
+ }
+
+ @Override
+ public void onCallStateButtonClicked() {
+ Intent broadcastIntent = Bindings.get(mContext).getCallStateButtonBroadcastIntent(mContext);
+ if (broadcastIntent != null) {
+ LogUtil.v(
+ "CallCardPresenter.onCallStateButtonClicked",
+ "sending call state button broadcast: " + broadcastIntent);
+ mContext.sendBroadcast(broadcastIntent, Manifest.permission.READ_PHONE_STATE);
+ }
+ }
+
+ @Override
+ public void onManageConferenceClicked() {
+ InCallActivity activity =
+ (InCallActivity) (mInCallScreen.getInCallScreenFragment().getActivity());
+ activity.showConferenceFragment(true);
+ }
+
+ @Override
+ public void onShrinkAnimationComplete() {
+ InCallPresenter.getInstance().onShrinkAnimationComplete();
+ }
+
+ @Override
+ public Drawable getDefaultContactPhotoDrawable() {
+ return ContactInfoCache.getInstance(mContext).getDefaultContactPhotoDrawable();
+ }
+
+ private void maybeStartSearch(DialerCall call, boolean isPrimary) {
+ // no need to start search for conference calls which show generic info.
+ if (call != null && !call.isConferenceCall()) {
+ startContactInfoSearch(call, isPrimary, call.getState() == DialerCall.State.INCOMING);
+ }
+ }
+
+ private void maybeClearSessionModificationState(DialerCall call) {
+ @SessionModificationState int state = call.getSessionModificationState();
+ if (state != DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST
+ && state != DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ LogUtil.i("CallCardPresenter.maybeClearSessionModificationState", "clearing state");
+ call.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+ }
+
+ /** Starts a query for more contact data for the save primary and secondary calls. */
+ private void startContactInfoSearch(
+ final DialerCall call, final boolean isPrimary, boolean isIncoming) {
+ final ContactInfoCache cache = ContactInfoCache.getInstance(mContext);
+
+ cache.findInfo(call, isIncoming, new ContactLookupCallback(this, isPrimary));
+ }
+
+ private void onContactInfoComplete(String callId, ContactCacheEntry entry, boolean isPrimary) {
+ final boolean entryMatchesExistingCall =
+ (isPrimary && mPrimary != null && TextUtils.equals(callId, mPrimary.getId()))
+ || (!isPrimary && mSecondary != null && TextUtils.equals(callId, mSecondary.getId()));
+ if (entryMatchesExistingCall) {
+ updateContactEntry(entry, isPrimary);
+ } else {
+ LogUtil.e(
+ "CallCardPresenter.onContactInfoComplete",
+ "dropping stale contact lookup info for " + callId);
+ }
+
+ final DialerCall call = CallList.getInstance().getCallById(callId);
+ if (call != null) {
+ call.getLogState().contactLookupResult = entry.contactLookupResult;
+ }
+ if (entry.contactUri != null) {
+ CallerInfoUtils.sendViewNotification(mContext, entry.contactUri);
+ }
+ }
+
+ private void onImageLoadComplete(String callId, ContactCacheEntry entry) {
+ if (getUi() == null) {
+ return;
+ }
+
+ if (entry.photo != null) {
+ if (mPrimary != null && callId.equals(mPrimary.getId())) {
+ updateContactEntry(entry, true /* isPrimary */);
+ } else if (mSecondary != null && callId.equals(mSecondary.getId())) {
+ updateContactEntry(entry, false /* isPrimary */);
+ }
+ }
+ }
+
+ private void updateContactEntry(ContactCacheEntry entry, boolean isPrimary) {
+ if (isPrimary) {
+ mPrimaryContactInfo = entry;
+ updatePrimaryDisplayInfo();
+ } else {
+ mSecondaryContactInfo = entry;
+ updateSecondaryDisplayInfo();
+ }
+ }
+
+ /**
+ * Get the highest priority call to display. Goes through the calls and chooses which to return
+ * based on priority of which type of call to display to the user. Callers can use the "ignore"
+ * feature to get the second best call by passing a previously found primary call as ignore.
+ *
+ * @param ignore A call to ignore if found.
+ */
+ private DialerCall getCallToDisplay(
+ CallList callList, DialerCall ignore, boolean skipDisconnected) {
+ // Active calls come second. An active call always gets precedent.
+ DialerCall retval = callList.getActiveCall();
+ if (retval != null && retval != ignore) {
+ return retval;
+ }
+
+ // Sometimes there is intemediate state that two calls are in active even one is about
+ // to be on hold.
+ retval = callList.getSecondActiveCall();
+ if (retval != null && retval != ignore) {
+ return retval;
+ }
+
+ // Disconnected calls get primary position if there are no active calls
+ // to let user know quickly what call has disconnected. Disconnected
+ // calls are very short lived.
+ if (!skipDisconnected) {
+ retval = callList.getDisconnectingCall();
+ if (retval != null && retval != ignore) {
+ return retval;
+ }
+ retval = callList.getDisconnectedCall();
+ if (retval != null && retval != ignore) {
+ return retval;
+ }
+ }
+
+ // Then we go to background call (calls on hold)
+ retval = callList.getBackgroundCall();
+ if (retval != null && retval != ignore) {
+ return retval;
+ }
+
+ // Lastly, we go to a second background call.
+ retval = callList.getSecondBackgroundCall();
+
+ return retval;
+ }
+
+ private void updatePrimaryDisplayInfo() {
+ if (mInCallScreen == null) {
+ // TODO: May also occur if search result comes back after ui is destroyed. Look into
+ // removing that case completely.
+ LogUtil.v(
+ "CallCardPresenter.updatePrimaryDisplayInfo",
+ "updatePrimaryDisplayInfo called but ui is null!");
+ return;
+ }
+
+ if (mPrimary == null) {
+ // Clear the primary display info.
+ mInCallScreen.setPrimary(PrimaryInfo.createEmptyPrimaryInfo());
+ return;
+ }
+
+ // Hide the contact photo if we are in a video call and the incoming video surface is
+ // showing.
+ boolean showContactPhoto =
+ !VideoCallPresenter.showIncomingVideo(mPrimary.getVideoState(), mPrimary.getState());
+
+ // DialerCall placed through a work phone account.
+ boolean hasWorkCallProperty = mPrimary.hasProperty(PROPERTY_ENTERPRISE_CALL);
+
+ Session enrichedCallSession =
+ mPrimary.getNumber() == null
+ ? null
+ : EnrichedCallManager.Accessor.getInstance(((Application) mContext))
+ .getSession(mPrimary.getNumber());
+ MultimediaData enrichedCallMultimediaData =
+ enrichedCallSession == null ? null : enrichedCallSession.getMultimediaData();
+
+ if (mPrimary.isConferenceCall()) {
+ LogUtil.v(
+ "CallCardPresenter.updatePrimaryDisplayInfo",
+ "update primary display info for conference call.");
+
+ mInCallScreen.setPrimary(
+ new PrimaryInfo(
+ null /* number */,
+ getConferenceString(mPrimary),
+ false /* nameIsNumber */,
+ null /* location */,
+ null /* label */,
+ getConferencePhoto(mPrimary),
+ ContactPhotoType.DEFAULT_PLACEHOLDER,
+ false /* isSipCall */,
+ showContactPhoto,
+ hasWorkCallProperty,
+ false /* isSpam */,
+ false /* answeringDisconnectsOngoingCall */,
+ shouldShowLocation(),
+ null /* contactInfoLookupKey */,
+ null /* enrichedCallMultimediaData */));
+ } else if (mPrimaryContactInfo != null) {
+ LogUtil.v(
+ "CallCardPresenter.updatePrimaryDisplayInfo",
+ "update primary display info for " + mPrimaryContactInfo);
+
+ String name = getNameForCall(mPrimaryContactInfo);
+ String number;
+
+ boolean isChildNumberShown = !TextUtils.isEmpty(mPrimary.getChildNumber());
+ boolean isForwardedNumberShown = !TextUtils.isEmpty(mPrimary.getLastForwardedNumber());
+ boolean isCallSubjectShown = shouldShowCallSubject(mPrimary);
+
+ if (isCallSubjectShown) {
+ number = null;
+ } else if (isChildNumberShown) {
+ number = mContext.getString(R.string.child_number, mPrimary.getChildNumber());
+ } else if (isForwardedNumberShown) {
+ // Use last forwarded number instead of second line, if present.
+ number = mPrimary.getLastForwardedNumber();
+ } else {
+ number = mPrimaryContactInfo.number;
+ }
+
+ boolean nameIsNumber = name != null && name.equals(mPrimaryContactInfo.number);
+ // DialerCall with caller that is a work contact.
+ boolean isWorkContact = (mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
+ mInCallScreen.setPrimary(
+ new PrimaryInfo(
+ number,
+ name,
+ nameIsNumber,
+ mPrimaryContactInfo.location,
+ isChildNumberShown || isCallSubjectShown ? null : mPrimaryContactInfo.label,
+ mPrimaryContactInfo.photo,
+ mPrimaryContactInfo.photoType,
+ mPrimaryContactInfo.isSipCall,
+ showContactPhoto,
+ hasWorkCallProperty || isWorkContact,
+ mPrimary.isSpam(),
+ mPrimary.answeringDisconnectsForegroundVideoCall(),
+ shouldShowLocation(),
+ mPrimaryContactInfo.lookupKey,
+ enrichedCallMultimediaData));
+ } else {
+ // Clear the primary display info.
+ mInCallScreen.setPrimary(PrimaryInfo.createEmptyPrimaryInfo());
+ }
+
+ mInCallScreen.showLocationUi(null);
+ }
+
+ private boolean shouldShowLocation() {
+ if (isOutgoingEmergencyCall(mPrimary)) {
+ LogUtil.i("CallCardPresenter.shouldShowLocation", "new emergency call");
+ return true;
+ } else if (isIncomingEmergencyCall(mPrimary)) {
+ LogUtil.i("CallCardPresenter.shouldShowLocation", "potential emergency callback");
+ return true;
+ } else if (isIncomingEmergencyCall(mSecondary)) {
+ LogUtil.i("CallCardPresenter.shouldShowLocation", "has potential emergency callback");
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean isOutgoingEmergencyCall(@Nullable DialerCall call) {
+ return call != null && !call.isIncoming() && call.isEmergencyCall();
+ }
+
+ private static boolean isIncomingEmergencyCall(@Nullable DialerCall call) {
+ return call != null && call.isIncoming() && call.isPotentialEmergencyCallback();
+ }
+
+ private boolean hasLocationPermission() {
+ return ContextCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_FINE_LOCATION)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ private boolean isBatteryTooLowForEmergencyLocation() {
+ Intent batteryStatus =
+ mContext.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+ int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
+ if (status == BatteryManager.BATTERY_STATUS_CHARGING
+ || status == BatteryManager.BATTERY_STATUS_FULL) {
+ // Plugged in or full battery
+ return false;
+ }
+ int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
+ int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
+ float batteryPercent = (100f * level) / scale;
+ long threshold =
+ ConfigProviderBindings.get(mContext)
+ .getLong(
+ CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION,
+ CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION_DEFAULT);
+ LogUtil.i(
+ "CallCardPresenter.isBatteryTooLowForEmergencyLocation",
+ "percent charged: " + batteryPercent + ", min required charge: " + threshold);
+ return batteryPercent < threshold;
+ }
+
+ private void updateSecondaryDisplayInfo() {
+ if (mInCallScreen == null) {
+ return;
+ }
+
+ if (mSecondary == null) {
+ // Clear the secondary display info.
+ mInCallScreen.setSecondary(SecondaryInfo.createEmptySecondaryInfo(mIsFullscreen));
+ return;
+ }
+
+ if (mSecondary.isConferenceCall()) {
+ mInCallScreen.setSecondary(
+ new SecondaryInfo(
+ true /* show */,
+ getConferenceString(mSecondary),
+ false /* nameIsNumber */,
+ null /* label */,
+ mSecondary.getCallProviderLabel(),
+ true /* isConference */,
+ mSecondary.isVideoCall(),
+ mIsFullscreen));
+ } else if (mSecondaryContactInfo != null) {
+ LogUtil.v("CallCardPresenter.updateSecondaryDisplayInfo", "" + mSecondaryContactInfo);
+ String name = getNameForCall(mSecondaryContactInfo);
+ boolean nameIsNumber = name != null && name.equals(mSecondaryContactInfo.number);
+ mInCallScreen.setSecondary(
+ new SecondaryInfo(
+ true /* show */,
+ name,
+ nameIsNumber,
+ mSecondaryContactInfo.label,
+ mSecondary.getCallProviderLabel(),
+ false /* isConference */,
+ mSecondary.isVideoCall(),
+ mIsFullscreen));
+ } else {
+ // Clear the secondary display info.
+ mInCallScreen.setSecondary(SecondaryInfo.createEmptySecondaryInfo(mIsFullscreen));
+ }
+ }
+
+ /** Returns the gateway number for any existing outgoing call. */
+ private String getGatewayNumber() {
+ if (hasOutgoingGatewayCall()) {
+ return DialerCall.getNumberFromHandle(mPrimary.getGatewayInfo().getGatewayAddress());
+ }
+ return null;
+ }
+
+ /**
+ * Returns the label (line of text above the number/name) for any given call. For example,
+ * "calling via [Account/Google Voice]" for outgoing calls.
+ */
+ private String getConnectionLabel() {
+ if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.READ_PHONE_STATE)
+ != PackageManager.PERMISSION_GRANTED) {
+ return null;
+ }
+ StatusHints statusHints = mPrimary.getStatusHints();
+ if (statusHints != null && !TextUtils.isEmpty(statusHints.getLabel())) {
+ return statusHints.getLabel().toString();
+ }
+
+ if (hasOutgoingGatewayCall() && getUi() != null) {
+ // Return the label for the gateway app on outgoing calls.
+ final PackageManager pm = mContext.getPackageManager();
+ try {
+ ApplicationInfo info =
+ pm.getApplicationInfo(mPrimary.getGatewayInfo().getGatewayProviderPackageName(), 0);
+ return pm.getApplicationLabel(info).toString();
+ } catch (PackageManager.NameNotFoundException e) {
+ LogUtil.e("CallCardPresenter.getConnectionLabel", "gateway Application Not Found.", e);
+ return null;
+ }
+ }
+ return mPrimary.getCallProviderLabel();
+ }
+
+ private Drawable getCallStateIcon() {
+ // Return connection icon if one exists.
+ StatusHints statusHints = mPrimary.getStatusHints();
+ if (statusHints != null && statusHints.getIcon() != null) {
+ Drawable icon = statusHints.getIcon().loadDrawable(mContext);
+ if (icon != null) {
+ return icon;
+ }
+ }
+
+ return null;
+ }
+
+ private boolean hasOutgoingGatewayCall() {
+ // We only display the gateway information while STATE_DIALING so return false for any other
+ // call state.
+ // TODO: mPrimary can be null because this is called from updatePrimaryDisplayInfo which
+ // is also called after a contact search completes (call is not present yet). Split the
+ // UI update so it can receive independent updates.
+ if (mPrimary == null) {
+ return false;
+ }
+ return DialerCall.State.isDialing(mPrimary.getState())
+ && mPrimary.getGatewayInfo() != null
+ && !mPrimary.getGatewayInfo().isEmpty();
+ }
+
+ /** Gets the name to display for the call. */
+ String getNameForCall(ContactCacheEntry contactInfo) {
+ String preferredName =
+ ContactDisplayUtils.getPreferredDisplayName(
+ contactInfo.namePrimary, contactInfo.nameAlternative, mContactsPreferences);
+ if (TextUtils.isEmpty(preferredName)) {
+ return contactInfo.number;
+ }
+ return preferredName;
+ }
+
+ /** Gets the number to display for a call. */
+ String getNumberForCall(ContactCacheEntry contactInfo) {
+ // If the name is empty, we use the number for the name...so don't show a second
+ // number in the number field
+ String preferredName =
+ ContactDisplayUtils.getPreferredDisplayName(
+ contactInfo.namePrimary, contactInfo.nameAlternative, mContactsPreferences);
+ if (TextUtils.isEmpty(preferredName)) {
+ return contactInfo.location;
+ }
+ return contactInfo.number;
+ }
+
+ @Override
+ public void onSecondaryInfoClicked() {
+ if (mSecondary == null) {
+ LogUtil.e(
+ "CallCardPresenter.onSecondaryInfoClicked",
+ "secondary info clicked but no secondary call.");
+ return;
+ }
+
+ LogUtil.i(
+ "CallCardPresenter.onSecondaryInfoClicked", "swapping call to foreground: " + mSecondary);
+ mSecondary.unhold();
+ }
+
+ @Override
+ public void onEndCallClicked() {
+ LogUtil.i("CallCardPresenter.onEndCallClicked", "disconnecting call: " + mPrimary);
+ if (mPrimary != null) {
+ mPrimary.disconnect();
+ }
+ }
+
+ /**
+ * Handles a change to the fullscreen mode of the in-call UI.
+ *
+ * @param isFullscreenMode {@code True} if the in-call UI is entering full screen mode.
+ */
+ @Override
+ public void onFullscreenModeChanged(boolean isFullscreenMode) {
+ mIsFullscreen = isFullscreenMode;
+ if (mInCallScreen == null) {
+ return;
+ }
+ maybeShowManageConferenceCallButton();
+ }
+
+ private boolean isPrimaryCallActive() {
+ return mPrimary != null && mPrimary.getState() == DialerCall.State.ACTIVE;
+ }
+
+ private String getConferenceString(DialerCall call) {
+ boolean isGenericConference = call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE);
+ LogUtil.v("CallCardPresenter.getConferenceString", "" + isGenericConference);
+
+ final int resId =
+ isGenericConference ? R.string.generic_conference_call_name : R.string.conference_call_name;
+ return mContext.getResources().getString(resId);
+ }
+
+ private Drawable getConferencePhoto(DialerCall call) {
+ boolean isGenericConference = call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE);
+ LogUtil.v("CallCardPresenter.getConferencePhoto", "" + isGenericConference);
+
+ final int resId = isGenericConference ? R.drawable.img_phone : R.drawable.img_conference;
+ Drawable photo = mContext.getResources().getDrawable(resId);
+ photo.setAutoMirrored(true);
+ return photo;
+ }
+
+ private boolean shouldShowEndCallButton(DialerCall primary, int callState) {
+ if (primary == null) {
+ return false;
+ }
+ if ((!DialerCall.State.isConnectingOrConnected(callState)
+ && callState != DialerCall.State.DISCONNECTING
+ && callState != DialerCall.State.DISCONNECTED)
+ || callState == DialerCall.State.INCOMING) {
+ return false;
+ }
+ if (mPrimary.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void onInCallScreenResumed() {
+ if (shouldSendAccessibilityEvent) {
+ handler.postDelayed(sendAccessibilityEventRunnable, ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS);
+ }
+ }
+
+ static boolean sendAccessibilityEvent(Context context, InCallScreen inCallScreen) {
+ AccessibilityManager am =
+ (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ if (!am.isEnabled()) {
+ LogUtil.w("CallCardPresenter.sendAccessibilityEvent", "accessibility is off");
+ return false;
+ }
+ if (inCallScreen == null) {
+ LogUtil.w("CallCardPresenter.sendAccessibilityEvent", "incallscreen is null");
+ return false;
+ }
+ Fragment fragment = inCallScreen.getInCallScreenFragment();
+ if (fragment == null || fragment.getView() == null || fragment.getView().getParent() == null) {
+ LogUtil.w("CallCardPresenter.sendAccessibilityEvent", "fragment/view/parent is null");
+ return false;
+ }
+
+ DisplayManager displayManager =
+ (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
+ Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
+ boolean screenIsOn = display.getState() == Display.STATE_ON;
+ LogUtil.d("CallCardPresenter.sendAccessibilityEvent", "screen is on: %b", screenIsOn);
+ if (!screenIsOn) {
+ return false;
+ }
+
+ AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT);
+ inCallScreen.dispatchPopulateAccessibilityEvent(event);
+ View view = inCallScreen.getInCallScreenFragment().getView();
+ view.getParent().requestSendAccessibilityEvent(view, event);
+ return true;
+ }
+
+ private void maybeSendAccessibilityEvent(
+ InCallState oldState, final InCallState newState, boolean primaryChanged) {
+ shouldSendAccessibilityEvent = false;
+ if (mContext == null) {
+ return;
+ }
+ final AccessibilityManager am =
+ (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ if (!am.isEnabled()) {
+ return;
+ }
+ // Announce the current call if it's new incoming/outgoing call or primary call is changed
+ // due to switching calls between two ongoing calls (one is on hold).
+ if ((oldState != InCallState.OUTGOING && newState == InCallState.OUTGOING)
+ || (oldState != InCallState.INCOMING && newState == InCallState.INCOMING)
+ || primaryChanged) {
+ LogUtil.i(
+ "CallCardPresenter.maybeSendAccessibilityEvent", "schedule accessibility announcement");
+ shouldSendAccessibilityEvent = true;
+ handler.postDelayed(sendAccessibilityEventRunnable, ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS);
+ }
+ }
+
+ /**
+ * Determines whether the call subject should be visible on the UI. For the call subject to be
+ * visible, the call has to be in an incoming or waiting state, and the subject must not be empty.
+ *
+ * @param call The call.
+ * @return {@code true} if the subject should be shown, {@code false} otherwise.
+ */
+ private boolean shouldShowCallSubject(DialerCall call) {
+ if (call == null) {
+ return false;
+ }
+
+ boolean isIncomingOrWaiting =
+ mPrimary.getState() == DialerCall.State.INCOMING
+ || mPrimary.getState() == DialerCall.State.CALL_WAITING;
+ return isIncomingOrWaiting
+ && !TextUtils.isEmpty(call.getCallSubject())
+ && call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED
+ && call.isCallSubjectSupported();
+ }
+
+ /**
+ * Determines whether the "note sent" toast should be shown. It should be shown for a new outgoing
+ * call with a subject.
+ *
+ * @param call The call
+ * @return {@code true} if the toast should be shown, {@code false} otherwise.
+ */
+ private boolean shouldShowNoteSentToast(DialerCall call) {
+ return call != null
+ && hasCallSubject(call)
+ && (call.getState() == DialerCall.State.DIALING
+ || call.getState() == DialerCall.State.CONNECTING);
+ }
+
+ private InCallScreen getUi() {
+ return mInCallScreen;
+ }
+
+ public static class ContactLookupCallback implements ContactInfoCacheCallback {
+
+ private final WeakReference<CallCardPresenter> mCallCardPresenter;
+ private final boolean mIsPrimary;
+
+ public ContactLookupCallback(CallCardPresenter callCardPresenter, boolean isPrimary) {
+ mCallCardPresenter = new WeakReference<CallCardPresenter>(callCardPresenter);
+ mIsPrimary = isPrimary;
+ }
+
+ @Override
+ public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
+ CallCardPresenter presenter = mCallCardPresenter.get();
+ if (presenter != null) {
+ presenter.onContactInfoComplete(callId, entry, mIsPrimary);
+ }
+ }
+
+ @Override
+ public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
+ CallCardPresenter presenter = mCallCardPresenter.get();
+ if (presenter != null) {
+ presenter.onImageLoadComplete(callId, entry);
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/CallerInfo.java b/java/com/android/incallui/CallerInfo.java
new file mode 100644
index 000000000..473bb8f4e
--- /dev/null
+++ b/java/com/android/incallui/CallerInfo.java
@@ -0,0 +1,573 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.PhoneLookup;
+import android.provider.ContactsContract.RawContacts;
+import android.support.annotation.RequiresApi;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.ContactsUtils.UserType;
+import com.android.contacts.common.util.TelephonyManagerUtils;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+import com.android.dialer.phonenumbercache.PhoneLookupUtil;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+
+/**
+ * Looks up caller information for the given phone number. This is intermediate data and should NOT
+ * be used by any UI.
+ */
+public class CallerInfo {
+
+ private static final String TAG = "CallerInfo";
+
+ // We should always use this projection starting from N onward.
+ @RequiresApi(VERSION_CODES.N)
+ private static final String[] DEFAULT_PHONELOOKUP_PROJECTION =
+ new String[] {
+ PhoneLookup.CONTACT_ID,
+ PhoneLookup.DISPLAY_NAME,
+ PhoneLookup.LOOKUP_KEY,
+ PhoneLookup.NUMBER,
+ PhoneLookup.NORMALIZED_NUMBER,
+ PhoneLookup.LABEL,
+ PhoneLookup.TYPE,
+ PhoneLookup.PHOTO_URI,
+ PhoneLookup.CUSTOM_RINGTONE,
+ PhoneLookup.SEND_TO_VOICEMAIL
+ };
+
+ // In pre-N, contact id is stored in {@link PhoneLookup._ID} in non-sip query.
+ private static final String[] BACKWARD_COMPATIBLE_NON_SIP_DEFAULT_PHONELOOKUP_PROJECTION =
+ new String[] {
+ PhoneLookup._ID,
+ PhoneLookup.DISPLAY_NAME,
+ PhoneLookup.LOOKUP_KEY,
+ PhoneLookup.NUMBER,
+ PhoneLookup.NORMALIZED_NUMBER,
+ PhoneLookup.LABEL,
+ PhoneLookup.TYPE,
+ PhoneLookup.PHOTO_URI,
+ PhoneLookup.CUSTOM_RINGTONE,
+ PhoneLookup.SEND_TO_VOICEMAIL
+ };
+ /**
+ * Please note that, any one of these member variables can be null, and any accesses to them
+ * should be prepared to handle such a case.
+ *
+ * <p>Also, it is implied that phoneNumber is more often populated than name is, (think of calls
+ * being dialed/received using numbers where names are not known to the device), so phoneNumber
+ * should serve as a dependable fallback when name is unavailable.
+ *
+ * <p>One other detail here is that this CallerInfo object reflects information found on a
+ * connection, it is an OUTPUT that serves mainly to display information to the user. In no way is
+ * this object used as input to make a connection, so we can choose to display whatever
+ * human-readable text makes sense to the user for a connection. This is especially relevant for
+ * the phone number field, since it is the one field that is most likely exposed to the user.
+ *
+ * <p>As an example: 1. User dials "911" 2. Device recognizes that this is an emergency number 3.
+ * We use the "Emergency Number" string instead of "911" in the phoneNumber field.
+ *
+ * <p>What we're really doing here is treating phoneNumber as an essential field here, NOT name.
+ * We're NOT always guaranteed to have a name for a connection, but the number should be
+ * displayable.
+ */
+ public String name;
+
+ public String nameAlternative;
+ public String phoneNumber;
+ public String normalizedNumber;
+ public String forwardingNumber;
+ public String geoDescription;
+ public String cnapName;
+ public int numberPresentation;
+ public int namePresentation;
+ public boolean contactExists;
+ public String phoneLabel;
+ /* Split up the phoneLabel into number type and label name */
+ public int numberType;
+ public String numberLabel;
+ public int photoResource;
+ // Contact ID, which will be 0 if a contact comes from the corp CP2.
+ public long contactIdOrZero;
+ public String lookupKeyOrNull;
+ public boolean needUpdate;
+ public Uri contactRefUri;
+ public @UserType long userType;
+ /**
+ * Contact display photo URI. If a contact has no display photo but a thumbnail, it'll be the
+ * thumbnail URI instead.
+ */
+ public Uri contactDisplayPhotoUri;
+ // fields to hold individual contact preference data,
+ // including the send to voicemail flag and the ringtone
+ // uri reference.
+ public Uri contactRingtoneUri;
+ public boolean shouldSendToVoicemail;
+ /**
+ * Drawable representing the caller image. This is essentially a cache for the image data tied
+ * into the connection / callerinfo object.
+ *
+ * <p>This might be a high resolution picture which is more suitable for full-screen image view
+ * than for smaller icons used in some kinds of notifications.
+ *
+ * <p>The {@link #isCachedPhotoCurrent} flag indicates if the image data needs to be reloaded.
+ */
+ public Drawable cachedPhoto;
+ /**
+ * Bitmap representing the caller image which has possibly lower resolution than {@link
+ * #cachedPhoto} and thus more suitable for icons (like notification icons).
+ *
+ * <p>In usual cases this is just down-scaled image of {@link #cachedPhoto}. If the down-scaling
+ * fails, this will just become null.
+ *
+ * <p>The {@link #isCachedPhotoCurrent} flag indicates if the image data needs to be reloaded.
+ */
+ public Bitmap cachedPhotoIcon;
+ /**
+ * Boolean which indicates if {@link #cachedPhoto} and {@link #cachedPhotoIcon} is fresh enough.
+ * If it is false, those images aren't pointing to valid objects.
+ */
+ public boolean isCachedPhotoCurrent;
+ /**
+ * String which holds the call subject sent as extra from the lower layers for this call. This is
+ * used to display the no-caller ID reason for restricted/unknown number presentation.
+ */
+ public String callSubject;
+
+ private boolean mIsEmergency;
+ private boolean mIsVoiceMail;
+
+ public CallerInfo() {
+ // TODO: Move all the basic initialization here?
+ mIsEmergency = false;
+ mIsVoiceMail = false;
+ userType = ContactsUtils.USER_TYPE_CURRENT;
+ }
+
+ public static String[] getDefaultPhoneLookupProjection(Uri phoneLookupUri) {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return DEFAULT_PHONELOOKUP_PROJECTION;
+ }
+ // Pre-N
+ boolean isSip =
+ phoneLookupUri.getBooleanQueryParameter(
+ ContactsContract.PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, false);
+ return (isSip)
+ ? DEFAULT_PHONELOOKUP_PROJECTION
+ : BACKWARD_COMPATIBLE_NON_SIP_DEFAULT_PHONELOOKUP_PROJECTION;
+ }
+
+ /**
+ * getCallerInfo given a Cursor.
+ *
+ * @param context the context used to retrieve string constants
+ * @param contactRef the URI to attach to this CallerInfo object
+ * @param cursor the first object in the cursor is used to build the CallerInfo object.
+ * @return the CallerInfo which contains the caller id for the given number. The returned
+ * CallerInfo is null if no number is supplied.
+ */
+ public static CallerInfo getCallerInfo(Context context, Uri contactRef, Cursor cursor) {
+ CallerInfo info = new CallerInfo();
+ info.photoResource = 0;
+ info.phoneLabel = null;
+ info.numberType = 0;
+ info.numberLabel = null;
+ info.cachedPhoto = null;
+ info.isCachedPhotoCurrent = false;
+ info.contactExists = false;
+ info.userType = ContactsUtils.USER_TYPE_CURRENT;
+
+ Log.v(TAG, "getCallerInfo() based on cursor...");
+
+ if (cursor != null) {
+ if (cursor.moveToFirst()) {
+ // TODO: photo_id is always available but not taken
+ // care of here. Maybe we should store it in the
+ // CallerInfo object as well.
+
+ long contactId = 0L;
+ int columnIndex;
+
+ // Look for the name
+ columnIndex = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME);
+ if (columnIndex != -1) {
+ info.name = cursor.getString(columnIndex);
+ }
+
+ // Look for the number
+ columnIndex = cursor.getColumnIndex(PhoneLookup.NUMBER);
+ if (columnIndex != -1) {
+ info.phoneNumber = cursor.getString(columnIndex);
+ }
+
+ // Look for the normalized number
+ columnIndex = cursor.getColumnIndex(PhoneLookup.NORMALIZED_NUMBER);
+ if (columnIndex != -1) {
+ info.normalizedNumber = cursor.getString(columnIndex);
+ }
+
+ // Look for the label/type combo
+ columnIndex = cursor.getColumnIndex(PhoneLookup.LABEL);
+ if (columnIndex != -1) {
+ int typeColumnIndex = cursor.getColumnIndex(PhoneLookup.TYPE);
+ if (typeColumnIndex != -1) {
+ info.numberType = cursor.getInt(typeColumnIndex);
+ info.numberLabel = cursor.getString(columnIndex);
+ info.phoneLabel =
+ Phone.getTypeLabel(context.getResources(), info.numberType, info.numberLabel)
+ .toString();
+ }
+ }
+
+ // cache the lookup key for later use to create lookup URIs
+ columnIndex = cursor.getColumnIndex(PhoneLookup.LOOKUP_KEY);
+ if (columnIndex != -1) {
+ info.lookupKeyOrNull = cursor.getString(columnIndex);
+ }
+
+ // Look for the person_id.
+ columnIndex = getColumnIndexForPersonId(contactRef, cursor);
+ if (columnIndex != -1) {
+ contactId = cursor.getLong(columnIndex);
+ // QuickContacts in M doesn't support enterprise contact id
+ if (contactId != 0
+ && (VERSION.SDK_INT >= VERSION_CODES.N
+ || !Contacts.isEnterpriseContactId(contactId))) {
+ info.contactIdOrZero = contactId;
+ Log.v(TAG, "==> got info.contactIdOrZero: " + info.contactIdOrZero);
+ }
+ } else {
+ // No valid columnIndex, so we can't look up person_id.
+ Log.v(TAG, "Couldn't find contactId column for " + contactRef);
+ // Watch out: this means that anything that depends on
+ // person_id will be broken (like contact photo lookups in
+ // the in-call UI, for example.)
+ }
+
+ // Display photo URI.
+ columnIndex = cursor.getColumnIndex(PhoneLookup.PHOTO_URI);
+ if ((columnIndex != -1) && (cursor.getString(columnIndex) != null)) {
+ info.contactDisplayPhotoUri = Uri.parse(cursor.getString(columnIndex));
+ } else {
+ info.contactDisplayPhotoUri = null;
+ }
+
+ // look for the custom ringtone, create from the string stored
+ // in the database.
+ columnIndex = cursor.getColumnIndex(PhoneLookup.CUSTOM_RINGTONE);
+ if ((columnIndex != -1) && (cursor.getString(columnIndex) != null)) {
+ if (TextUtils.isEmpty(cursor.getString(columnIndex))) {
+ // make it consistent with frameworks/base/.../CallerInfo.java
+ info.contactRingtoneUri = Uri.EMPTY;
+ } else {
+ info.contactRingtoneUri = Uri.parse(cursor.getString(columnIndex));
+ }
+ } else {
+ info.contactRingtoneUri = null;
+ }
+
+ // look for the send to voicemail flag, set it to true only
+ // under certain circumstances.
+ columnIndex = cursor.getColumnIndex(PhoneLookup.SEND_TO_VOICEMAIL);
+ info.shouldSendToVoicemail = (columnIndex != -1) && ((cursor.getInt(columnIndex)) == 1);
+ info.contactExists = true;
+
+ // Determine userType by directoryId and contactId
+ final String directory =
+ contactRef == null
+ ? null
+ : contactRef.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
+ Long directoryId = null;
+ if (directory != null) {
+ try {
+ directoryId = Long.parseLong(directory);
+ } catch (NumberFormatException e) {
+ // do nothing
+ }
+ }
+ info.userType = ContactsUtils.determineUserType(directoryId, contactId);
+
+ info.nameAlternative =
+ ContactInfoHelper.lookUpDisplayNameAlternative(
+ context, info.lookupKeyOrNull, info.userType, directoryId);
+ }
+ cursor.close();
+ }
+
+ info.needUpdate = false;
+ info.name = normalize(info.name);
+ info.contactRefUri = contactRef;
+
+ return info;
+ }
+
+ /**
+ * getCallerInfo given a URI, look up in the call-log database for the uri unique key.
+ *
+ * @param context the context used to get the ContentResolver
+ * @param contactRef the URI used to lookup caller id
+ * @return the CallerInfo which contains the caller id for the given number. The returned
+ * CallerInfo is null if no number is supplied.
+ */
+ private static CallerInfo getCallerInfo(Context context, Uri contactRef) {
+
+ return getCallerInfo(
+ context,
+ contactRef,
+ context.getContentResolver().query(contactRef, null, null, null, null));
+ }
+
+ /**
+ * Performs another lookup if previous lookup fails and it's a SIP call and the peer's username is
+ * all numeric. Look up the username as it could be a PSTN number in the contact database.
+ *
+ * @param context the query context
+ * @param number the original phone number, could be a SIP URI
+ * @param previousResult the result of previous lookup
+ * @return previousResult if it's not the case
+ */
+ static CallerInfo doSecondaryLookupIfNecessary(
+ Context context, String number, CallerInfo previousResult) {
+ if (!previousResult.contactExists && PhoneNumberHelper.isUriNumber(number)) {
+ String username = PhoneNumberHelper.getUsernameFromUriNumber(number);
+ if (PhoneNumberUtils.isGlobalPhoneNumber(username)) {
+ previousResult =
+ getCallerInfo(
+ context,
+ Uri.withAppendedPath(
+ PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, Uri.encode(username)));
+ }
+ }
+ return previousResult;
+ }
+
+ // Accessors
+
+ private static String normalize(String s) {
+ if (s == null || s.length() > 0) {
+ return s;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the column index to use to find the "person_id" field in the specified cursor, based on
+ * the contact URI that was originally queried.
+ *
+ * <p>This is a helper function for the getCallerInfo() method that takes a Cursor. Looking up the
+ * person_id is nontrivial (compared to all the other CallerInfo fields) since the column we need
+ * to use depends on what query we originally ran.
+ *
+ * <p>Watch out: be sure to not do any database access in this method, since it's run from the UI
+ * thread (see comments below for more info.)
+ *
+ * @return the columnIndex to use (with cursor.getLong()) to get the person_id, or -1 if we
+ * couldn't figure out what colum to use.
+ * <p>TODO: Add a unittest for this method. (This is a little tricky to test, since we'll need
+ * a live contacts database to test against, preloaded with at least some phone numbers and
+ * SIP addresses. And we'll probably have to hardcode the column indexes we expect, so the
+ * test might break whenever the contacts schema changes. But we can at least make sure we
+ * handle all the URI patterns we claim to, and that the mime types match what we expect...)
+ */
+ private static int getColumnIndexForPersonId(Uri contactRef, Cursor cursor) {
+ // TODO: This is pretty ugly now, see bug 2269240 for
+ // more details. The column to use depends upon the type of URL:
+ // - content://com.android.contacts/data/phones ==> use the "contact_id" column
+ // - content://com.android.contacts/phone_lookup ==> use the "_ID" column
+ // - content://com.android.contacts/data ==> use the "contact_id" column
+ // If it's none of the above, we leave columnIndex=-1 which means
+ // that the person_id field will be left unset.
+ //
+ // The logic here *used* to be based on the mime type of contactRef
+ // (for example Phone.CONTENT_ITEM_TYPE would tell us to use the
+ // RawContacts.CONTACT_ID column). But looking up the mime type requires
+ // a call to context.getContentResolver().getType(contactRef), which
+ // isn't safe to do from the UI thread since it can cause an ANR if
+ // the contacts provider is slow or blocked (like during a sync.)
+ //
+ // So instead, figure out the column to use for person_id by just
+ // looking at the URI itself.
+
+ Log.v(TAG, "- getColumnIndexForPersonId: contactRef URI = '" + contactRef + "'...");
+ // Warning: Do not enable the following logging (due to ANR risk.)
+ // if (VDBG) Rlog.v(TAG, "- MIME type: "
+ // + context.getContentResolver().getType(contactRef));
+
+ String url = contactRef.toString();
+ String columnName = null;
+ if (url.startsWith("content://com.android.contacts/data/phones")) {
+ // Direct lookup in the Phone table.
+ // MIME type: Phone.CONTENT_ITEM_TYPE (= "vnd.android.cursor.item/phone_v2")
+ Log.v(TAG, "'data/phones' URI; using RawContacts.CONTACT_ID");
+ columnName = RawContacts.CONTACT_ID;
+ } else if (url.startsWith("content://com.android.contacts/data")) {
+ // Direct lookup in the Data table.
+ // MIME type: Data.CONTENT_TYPE (= "vnd.android.cursor.dir/data")
+ Log.v(TAG, "'data' URI; using Data.CONTACT_ID");
+ // (Note Data.CONTACT_ID and RawContacts.CONTACT_ID are equivalent.)
+ columnName = Data.CONTACT_ID;
+ } else if (url.startsWith("content://com.android.contacts/phone_lookup")) {
+ // Lookup in the PhoneLookup table, which provides "fuzzy matching"
+ // for phone numbers.
+ // MIME type: PhoneLookup.CONTENT_TYPE (= "vnd.android.cursor.dir/phone_lookup")
+ Log.v(TAG, "'phone_lookup' URI; using PhoneLookup._ID");
+ columnName = PhoneLookupUtil.getContactIdColumnNameForUri(contactRef);
+ } else {
+ Log.v(TAG, "Unexpected prefix for contactRef '" + url + "'");
+ }
+ int columnIndex = (columnName != null) ? cursor.getColumnIndex(columnName) : -1;
+ Log.v(
+ TAG,
+ "==> Using column '"
+ + columnName
+ + "' (columnIndex = "
+ + columnIndex
+ + ") for person_id lookup...");
+ return columnIndex;
+ }
+
+ /** @return true if the caller info is an emergency number. */
+ public boolean isEmergencyNumber() {
+ return mIsEmergency;
+ }
+
+ /** @return true if the caller info is a voicemail number. */
+ public boolean isVoiceMailNumber() {
+ return mIsVoiceMail;
+ }
+
+ /**
+ * Mark this CallerInfo as an emergency call.
+ *
+ * @param context To lookup the localized 'Emergency Number' string.
+ * @return this instance.
+ */
+ /* package */ CallerInfo markAsEmergency(Context context) {
+ name = context.getString(R.string.emergency_call_dialog_number_for_display);
+ phoneNumber = null;
+
+ photoResource = R.drawable.img_phone;
+ mIsEmergency = true;
+ return this;
+ }
+
+ /**
+ * Mark this CallerInfo as a voicemail call. The voicemail label is obtained from the telephony
+ * manager. Caller must hold the READ_PHONE_STATE permission otherwise the phoneNumber will be set
+ * to null.
+ *
+ * @return this instance.
+ */
+ /* package */ CallerInfo markAsVoiceMail(Context context) {
+ mIsVoiceMail = true;
+
+ try {
+ // For voicemail calls, we display the voice mail tag
+ // instead of the real phone number in the "number"
+ // field.
+ name = TelephonyManagerUtils.getVoiceMailAlphaTag(context);
+ phoneNumber = null;
+ } catch (SecurityException se) {
+ // Should never happen: if this process does not have
+ // permission to retrieve VM tag, it should not have
+ // permission to retrieve VM number and would not call
+ // this method.
+ // Leave phoneNumber untouched.
+ Log.e(TAG, "Cannot access VoiceMail.", se);
+ }
+ // TODO: There is no voicemail picture?
+ // FIXME: FIND ANOTHER ICON
+ // photoResource = android.R.drawable.badge_voicemail;
+ return this;
+ }
+
+ /**
+ * Updates this CallerInfo's geoDescription field, based on the raw phone number in the
+ * phoneNumber field.
+ *
+ * <p>(Note that the various getCallerInfo() methods do *not* set the geoDescription
+ * automatically; you need to call this method explicitly to get it.)
+ *
+ * @param context the context used to look up the current locale / country
+ * @param fallbackNumber if this CallerInfo's phoneNumber field is empty, this specifies a
+ * fallback number to use instead.
+ */
+ public void updateGeoDescription(Context context, String fallbackNumber) {
+ String number = TextUtils.isEmpty(phoneNumber) ? fallbackNumber : phoneNumber;
+ geoDescription = PhoneNumberHelper.getGeoDescription(context, number);
+ }
+
+ /** @return a string debug representation of this instance. */
+ @Override
+ public String toString() {
+ // Warning: never check in this file with VERBOSE_DEBUG = true
+ // because that will result in PII in the system log.
+ final boolean VERBOSE_DEBUG = false;
+
+ if (VERBOSE_DEBUG) {
+ return new StringBuilder(384)
+ .append(super.toString() + " { ")
+ .append("\nname: " + name)
+ .append("\nphoneNumber: " + phoneNumber)
+ .append("\nnormalizedNumber: " + normalizedNumber)
+ .append("\forwardingNumber: " + forwardingNumber)
+ .append("\ngeoDescription: " + geoDescription)
+ .append("\ncnapName: " + cnapName)
+ .append("\nnumberPresentation: " + numberPresentation)
+ .append("\nnamePresentation: " + namePresentation)
+ .append("\ncontactExists: " + contactExists)
+ .append("\nphoneLabel: " + phoneLabel)
+ .append("\nnumberType: " + numberType)
+ .append("\nnumberLabel: " + numberLabel)
+ .append("\nphotoResource: " + photoResource)
+ .append("\ncontactIdOrZero: " + contactIdOrZero)
+ .append("\nneedUpdate: " + needUpdate)
+ .append("\ncontactRefUri: " + contactRefUri)
+ .append("\ncontactRingtoneUri: " + contactRingtoneUri)
+ .append("\ncontactDisplayPhotoUri: " + contactDisplayPhotoUri)
+ .append("\nshouldSendToVoicemail: " + shouldSendToVoicemail)
+ .append("\ncachedPhoto: " + cachedPhoto)
+ .append("\nisCachedPhotoCurrent: " + isCachedPhotoCurrent)
+ .append("\nemergency: " + mIsEmergency)
+ .append("\nvoicemail: " + mIsVoiceMail)
+ .append("\nuserType: " + userType)
+ .append(" }")
+ .toString();
+ } else {
+ return new StringBuilder(128)
+ .append(super.toString() + " { ")
+ .append("name " + ((name == null) ? "null" : "non-null"))
+ .append(", phoneNumber " + ((phoneNumber == null) ? "null" : "non-null"))
+ .append(" }")
+ .toString();
+ }
+ }
+}
diff --git a/java/com/android/incallui/CallerInfoAsyncQuery.java b/java/com/android/incallui/CallerInfoAsyncQuery.java
new file mode 100644
index 000000000..f8d7ac65a
--- /dev/null
+++ b/java/com/android/incallui/CallerInfoAsyncQuery.java
@@ -0,0 +1,638 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui;
+
+import android.Manifest;
+import android.annotation.TargetApi;
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Directory;
+import android.support.annotation.MainThread;
+import android.support.annotation.RequiresPermission;
+import android.support.annotation.WorkerThread;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import com.android.contacts.common.compat.DirectoryCompat;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+import com.android.dialer.phonenumbercache.PhoneNumberCache;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Helper class to make it easier to run asynchronous caller-id lookup queries.
+ *
+ * @see CallerInfo
+ */
+@TargetApi(VERSION_CODES.M)
+public class CallerInfoAsyncQuery {
+
+ /** Interface for a CallerInfoAsyncQueryHandler result return. */
+ public interface OnQueryCompleteListener {
+
+ /** Called when the query is complete. */
+ @MainThread
+ void onQueryComplete(int token, Object cookie, CallerInfo ci);
+
+ /** Called when data is loaded. Must be called in worker thread. */
+ @WorkerThread
+ void onDataLoaded(int token, Object cookie, CallerInfo ci);
+ }
+
+ private static final boolean DBG = false;
+ private static final String LOG_TAG = "CallerInfoAsyncQuery";
+
+ private static final int EVENT_NEW_QUERY = 1;
+ private static final int EVENT_ADD_LISTENER = 2;
+ private static final int EVENT_EMERGENCY_NUMBER = 3;
+ private static final int EVENT_VOICEMAIL_NUMBER = 4;
+ // If the CallerInfo query finds no contacts, should we use the
+ // PhoneNumberOfflineGeocoder to look up a "geo description"?
+ // (TODO: This could become a flag in config.xml if it ever needs to be
+ // configured on a per-product basis.)
+ private static final boolean ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION = true;
+ /* Directory lookup related code - START */
+ private static final String[] DIRECTORY_PROJECTION = new String[] {Directory._ID};
+
+ /** Private constructor for factory methods. */
+ private CallerInfoAsyncQuery() {}
+
+ @RequiresPermission(Manifest.permission.READ_CONTACTS)
+ public static void startQuery(
+ final int token,
+ final Context context,
+ final CallerInfo info,
+ final OnQueryCompleteListener listener,
+ final Object cookie) {
+ Log.d(LOG_TAG, "##### CallerInfoAsyncQuery startContactProviderQuery()... #####");
+ Log.d(LOG_TAG, "- number: " + info.phoneNumber);
+ Log.d(LOG_TAG, "- cookie: " + cookie);
+
+ OnQueryCompleteListener contactsProviderQueryCompleteListener =
+ new OnQueryCompleteListener() {
+ @Override
+ public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
+ Log.d(LOG_TAG, "contactsProviderQueryCompleteListener done");
+ // If there are no other directory queries, make sure that the listener is
+ // notified of this result. see b/27621628
+ if ((ci != null && ci.contactExists)
+ || !startOtherDirectoriesQuery(token, context, info, listener, cookie)) {
+ if (listener != null && ci != null) {
+ listener.onQueryComplete(token, cookie, ci);
+ }
+ }
+ }
+
+ @Override
+ public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
+ listener.onDataLoaded(token, cookie, ci);
+ }
+ };
+ startDefaultDirectoryQuery(token, context, info, contactsProviderQueryCompleteListener, cookie);
+ }
+
+ // Private methods
+ private static void startDefaultDirectoryQuery(
+ int token,
+ Context context,
+ CallerInfo info,
+ OnQueryCompleteListener listener,
+ Object cookie) {
+ // Construct the URI object and query params, and start the query.
+ Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber);
+ startQueryInternal(token, context, info, listener, cookie, uri);
+ }
+
+ /**
+ * Factory method to start the query based on a CallerInfo object.
+ *
+ * <p>Note: if the number contains an "@" character we treat it as a SIP address, and look it up
+ * directly in the Data table rather than using the PhoneLookup table. TODO: But eventually we
+ * should expose two separate methods, one for numbers and one for SIP addresses, and then have
+ * PhoneUtils.startGetCallerInfo() decide which one to call based on the phone type of the
+ * incoming connection.
+ */
+ private static void startQueryInternal(
+ int token,
+ Context context,
+ CallerInfo info,
+ OnQueryCompleteListener listener,
+ Object cookie,
+ Uri contactRef) {
+ if (DBG) {
+ Log.d(LOG_TAG, "==> contactRef: " + sanitizeUriToString(contactRef));
+ }
+
+ if ((context == null) || (contactRef == null)) {
+ throw new QueryPoolException("Bad context or query uri.");
+ }
+ CallerInfoAsyncQueryHandler handler = new CallerInfoAsyncQueryHandler(context, contactRef);
+
+ //create cookieWrapper, start query
+ CookieWrapper cw = new CookieWrapper();
+ cw.listener = listener;
+ cw.cookie = cookie;
+ cw.number = info.phoneNumber;
+
+ // check to see if these are recognized numbers, and use shortcuts if we can.
+ if (PhoneNumberUtils.isLocalEmergencyNumber(context, info.phoneNumber)) {
+ cw.event = EVENT_EMERGENCY_NUMBER;
+ } else if (info.isVoiceMailNumber()) {
+ cw.event = EVENT_VOICEMAIL_NUMBER;
+ } else {
+ cw.event = EVENT_NEW_QUERY;
+ }
+
+ String[] proejection = CallerInfo.getDefaultPhoneLookupProjection(contactRef);
+ handler.startQuery(
+ token,
+ cw, // cookie
+ contactRef, // uri
+ proejection, // projection
+ null, // selection
+ null, // selectionArgs
+ null); // orderBy
+ }
+
+ // Return value indicates if listener was notified.
+ private static boolean startOtherDirectoriesQuery(
+ int token,
+ Context context,
+ CallerInfo info,
+ OnQueryCompleteListener listener,
+ Object cookie) {
+ long[] directoryIds = getDirectoryIds(context);
+ int size = directoryIds.length;
+ if (size == 0) {
+ return false;
+ }
+
+ DirectoryQueryCompleteListenerFactory listenerFactory =
+ new DirectoryQueryCompleteListenerFactory(context, size, listener);
+
+ // The current implementation of multiple async query runs in single handler thread
+ // in AsyncQueryHandler.
+ // intermediateListener.onQueryComplete is also called from the same caller thread.
+ // TODO(b/26019872): use thread pool instead of single thread.
+ for (int i = 0; i < size; i++) {
+ long directoryId = directoryIds[i];
+ Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber, directoryId);
+ if (DBG) {
+ Log.d(LOG_TAG, "directoryId: " + directoryId + " uri: " + uri);
+ }
+ OnQueryCompleteListener intermediateListener = listenerFactory.newListener(directoryId);
+ startQueryInternal(token, context, info, intermediateListener, cookie, uri);
+ }
+ return true;
+ }
+
+ private static long[] getDirectoryIds(Context context) {
+ ArrayList<Long> results = new ArrayList<>();
+
+ Uri uri = Directory.CONTENT_URI;
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ uri = Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "directories_enterprise");
+ }
+
+ ContentResolver cr = context.getContentResolver();
+ Cursor cursor = cr.query(uri, DIRECTORY_PROJECTION, null, null, null);
+ addDirectoryIdsFromCursor(cursor, results);
+
+ long[] result = new long[results.size()];
+ for (int i = 0; i < results.size(); i++) {
+ result[i] = results.get(i);
+ }
+ return result;
+ }
+
+ private static void addDirectoryIdsFromCursor(Cursor cursor, ArrayList<Long> results) {
+ if (cursor != null) {
+ int idIndex = cursor.getColumnIndex(Directory._ID);
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(idIndex);
+ if (DirectoryCompat.isRemoteDirectoryId(id)) {
+ results.add(id);
+ }
+ }
+ cursor.close();
+ }
+ }
+
+ private static String sanitizeUriToString(Uri uri) {
+ if (uri != null) {
+ String uriString = uri.toString();
+ int indexOfLastSlash = uriString.lastIndexOf('/');
+ if (indexOfLastSlash > 0) {
+ return uriString.substring(0, indexOfLastSlash) + "/xxxxxxx";
+ } else {
+ return uriString;
+ }
+ } else {
+ return "";
+ }
+ }
+
+ /** Wrap the cookie from the WorkerArgs with additional information needed by our classes. */
+ private static final class CookieWrapper {
+
+ public OnQueryCompleteListener listener;
+ public Object cookie;
+ public int event;
+ public String number;
+ }
+ /* Directory lookup related code - END */
+
+ /** Simple exception used to communicate problems with the query pool. */
+ public static class QueryPoolException extends SQLException {
+
+ public QueryPoolException(String error) {
+ super(error);
+ }
+ }
+
+ private static final class DirectoryQueryCompleteListenerFactory {
+
+ private final OnQueryCompleteListener mListener;
+ private final Context mContext;
+ // Make sure listener to be called once and only once
+ private int mCount;
+ private boolean mIsListenerCalled;
+
+ DirectoryQueryCompleteListenerFactory(
+ Context context, int size, OnQueryCompleteListener listener) {
+ mCount = size;
+ mListener = listener;
+ mIsListenerCalled = false;
+ mContext = context;
+ }
+
+ private void onDirectoryQueryComplete(
+ int token, Object cookie, CallerInfo ci, long directoryId) {
+ boolean shouldCallListener = false;
+ synchronized (this) {
+ mCount = mCount - 1;
+ if (!mIsListenerCalled && (ci.contactExists || mCount == 0)) {
+ mIsListenerCalled = true;
+ shouldCallListener = true;
+ }
+ }
+
+ // Don't call callback in synchronized block because mListener.onQueryComplete may
+ // take long time to complete
+ if (shouldCallListener && mListener != null) {
+ addCallerInfoIntoCache(ci, directoryId);
+ mListener.onQueryComplete(token, cookie, ci);
+ }
+ }
+
+ private void addCallerInfoIntoCache(CallerInfo ci, long directoryId) {
+ CachedNumberLookupService cachedNumberLookupService =
+ PhoneNumberCache.get(mContext).getCachedNumberLookupService();
+ if (ci.contactExists && cachedNumberLookupService != null) {
+ // 1. Cache caller info
+ CachedContactInfo cachedContactInfo =
+ CallerInfoUtils.buildCachedContactInfo(cachedNumberLookupService, ci);
+ String directoryLabel = mContext.getString(R.string.directory_search_label);
+ cachedContactInfo.setDirectorySource(directoryLabel, directoryId);
+ cachedNumberLookupService.addContact(mContext, cachedContactInfo);
+
+ // 2. Cache photo
+ if (ci.contactDisplayPhotoUri != null && ci.normalizedNumber != null) {
+ try (InputStream in =
+ mContext.getContentResolver().openInputStream(ci.contactDisplayPhotoUri)) {
+ if (in != null) {
+ cachedNumberLookupService.addPhoto(mContext, ci.normalizedNumber, in);
+ }
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "failed to fetch directory contact photo", e);
+ }
+ }
+ }
+ }
+
+ public OnQueryCompleteListener newListener(long directoryId) {
+ return new DirectoryQueryCompleteListener(directoryId);
+ }
+
+ private class DirectoryQueryCompleteListener implements OnQueryCompleteListener {
+
+ private final long mDirectoryId;
+
+ DirectoryQueryCompleteListener(long directoryId) {
+ mDirectoryId = directoryId;
+ }
+
+ @Override
+ public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
+ mListener.onDataLoaded(token, cookie, ci);
+ }
+
+ @Override
+ public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
+ onDirectoryQueryComplete(token, cookie, ci, mDirectoryId);
+ }
+ }
+ }
+
+ /** Our own implementation of the AsyncQueryHandler. */
+ private static class CallerInfoAsyncQueryHandler extends AsyncQueryHandler {
+
+ /**
+ * The information relevant to each CallerInfo query. Each query may have multiple listeners, so
+ * each AsyncCursorInfo is associated with 2 or more CookieWrapper objects in the queue (one
+ * with a new query event, and one with a end event, with 0 or more additional listeners in
+ * between).
+ */
+ private Context mQueryContext;
+
+ private Uri mQueryUri;
+ private CallerInfo mCallerInfo;
+
+ /** Asynchronous query handler class for the contact / callerinfo object. */
+ private CallerInfoAsyncQueryHandler(Context context, Uri contactRef) {
+ super(context.getContentResolver());
+ this.mQueryContext = context;
+ this.mQueryUri = contactRef;
+ }
+
+ @Override
+ public void startQuery(
+ int token,
+ Object cookie,
+ Uri uri,
+ String[] projection,
+ String selection,
+ String[] selectionArgs,
+ String orderBy) {
+ if (DBG) {
+ // Show stack trace with the arguments.
+ Log.d(
+ LOG_TAG,
+ "InCall: startQuery: url="
+ + uri
+ + " projection=["
+ + Arrays.toString(projection)
+ + "]"
+ + " selection="
+ + selection
+ + " "
+ + " args=["
+ + Arrays.toString(selectionArgs)
+ + "]",
+ new RuntimeException("STACKTRACE"));
+ }
+ super.startQuery(token, cookie, uri, projection, selection, selectionArgs, orderBy);
+ }
+
+ @Override
+ protected Handler createHandler(Looper looper) {
+ return new CallerInfoWorkerHandler(looper);
+ }
+
+ /**
+ * Overrides onQueryComplete from AsyncQueryHandler.
+ *
+ * <p>This method takes into account the state of this class; we construct the CallerInfo object
+ * only once for each set of listeners. When the query thread has done its work and calls this
+ * method, we inform the remaining listeners in the queue, until we're out of listeners. Once we
+ * get the message indicating that we should expect no new listeners for this CallerInfo object,
+ * we release the AsyncCursorInfo back into the pool.
+ */
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ Log.d(this, "##### onQueryComplete() ##### query complete for token: " + token);
+
+ CookieWrapper cw = (CookieWrapper) cookie;
+
+ if (cw.listener != null) {
+ Log.d(
+ this,
+ "notifying listener: "
+ + cw.listener.getClass().toString()
+ + " for token: "
+ + token
+ + mCallerInfo);
+ cw.listener.onQueryComplete(token, cw.cookie, mCallerInfo);
+ }
+ mQueryContext = null;
+ mQueryUri = null;
+ mCallerInfo = null;
+ }
+
+ protected void updateData(int token, Object cookie, Cursor cursor) {
+ try {
+ Log.d(this, "##### updateData() ##### for token: " + token);
+
+ //get the cookie and notify the listener.
+ CookieWrapper cw = (CookieWrapper) cookie;
+ if (cw == null) {
+ // Normally, this should never be the case for calls originating
+ // from within this code.
+ // However, if there is any code that calls this method, we should
+ // check the parameters to make sure they're viable.
+ Log.d(this, "Cookie is null, ignoring onQueryComplete() request.");
+ return;
+ }
+
+ // check the token and if needed, create the callerinfo object.
+ if (mCallerInfo == null) {
+ if ((mQueryContext == null) || (mQueryUri == null)) {
+ throw new QueryPoolException(
+ "Bad context or query uri, or CallerInfoAsyncQuery already released.");
+ }
+
+ // adjust the callerInfo data as needed, and only if it was set from the
+ // initial query request.
+ // Change the callerInfo number ONLY if it is an emergency number or the
+ // voicemail number, and adjust other data (including photoResource)
+ // accordingly.
+ if (cw.event == EVENT_EMERGENCY_NUMBER) {
+ // Note we're setting the phone number here (refer to javadoc
+ // comments at the top of CallerInfo class).
+ mCallerInfo = new CallerInfo().markAsEmergency(mQueryContext);
+ } else if (cw.event == EVENT_VOICEMAIL_NUMBER) {
+ mCallerInfo = new CallerInfo().markAsVoiceMail(mQueryContext);
+ } else {
+ mCallerInfo = CallerInfo.getCallerInfo(mQueryContext, mQueryUri, cursor);
+ Log.d(this, "==> Got mCallerInfo: " + mCallerInfo);
+
+ CallerInfo newCallerInfo =
+ CallerInfo.doSecondaryLookupIfNecessary(mQueryContext, cw.number, mCallerInfo);
+ if (newCallerInfo != mCallerInfo) {
+ mCallerInfo = newCallerInfo;
+ Log.d(this, "#####async contact look up with numeric username" + mCallerInfo);
+ }
+
+ // Final step: look up the geocoded description.
+ if (ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION) {
+ // Note we do this only if we *don't* have a valid name (i.e. if
+ // no contacts matched the phone number of the incoming call),
+ // since that's the only case where the incoming-call UI cares
+ // about this field.
+ //
+ // (TODO: But if we ever want the UI to show the geoDescription
+ // even when we *do* match a contact, we'll need to either call
+ // updateGeoDescription() unconditionally here, or possibly add a
+ // new parameter to CallerInfoAsyncQuery.startQuery() to force
+ // the geoDescription field to be populated.)
+
+ if (TextUtils.isEmpty(mCallerInfo.name)) {
+ // Actually when no contacts match the incoming phone number,
+ // the CallerInfo object is totally blank here (i.e. no name
+ // *or* phoneNumber). So we need to pass in cw.number as
+ // a fallback number.
+ mCallerInfo.updateGeoDescription(mQueryContext, cw.number);
+ }
+ }
+
+ // Use the number entered by the user for display.
+ if (!TextUtils.isEmpty(cw.number)) {
+ mCallerInfo.phoneNumber = cw.number;
+ }
+ }
+
+ Log.d(this, "constructing CallerInfo object for token: " + token);
+
+ if (cw.listener != null) {
+ cw.listener.onDataLoaded(token, cw.cookie, mCallerInfo);
+ }
+ }
+
+ } finally {
+ // The cursor may have been closed in CallerInfo.getCallerInfo()
+ if (cursor != null && !cursor.isClosed()) {
+ cursor.close();
+ }
+ }
+ }
+
+ /**
+ * Our own query worker thread.
+ *
+ * <p>This thread handles the messages enqueued in the looper. The normal sequence of events is
+ * that a new query shows up in the looper queue, followed by 0 or more add listener requests,
+ * and then an end request. Of course, these requests can be interlaced with requests from other
+ * tokens, but is irrelevant to this handler since the handler has no state.
+ *
+ * <p>Note that we depend on the queue to keep things in order; in other words, the looper queue
+ * must be FIFO with respect to input from the synchronous startQuery calls and output to this
+ * handleMessage call.
+ *
+ * <p>This use of the queue is required because CallerInfo objects may be accessed multiple
+ * times before the query is complete. All accesses (listeners) must be queued up and informed
+ * in order when the query is complete.
+ */
+ protected class CallerInfoWorkerHandler extends WorkerHandler {
+
+ public CallerInfoWorkerHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ WorkerArgs args = (WorkerArgs) msg.obj;
+ CookieWrapper cw = (CookieWrapper) args.cookie;
+
+ if (cw == null) {
+ // Normally, this should never be the case for calls originating
+ // from within this code.
+ // However, if there is any code that this Handler calls (such as in
+ // super.handleMessage) that DOES place unexpected messages on the
+ // queue, then we need pass these messages on.
+ Log.d(
+ this,
+ "Unexpected command (CookieWrapper is null): "
+ + msg.what
+ + " ignored by CallerInfoWorkerHandler, passing onto parent.");
+
+ super.handleMessage(msg);
+ } else {
+ Log.d(
+ this,
+ "Processing event: "
+ + cw.event
+ + " token (arg1): "
+ + msg.arg1
+ + " command: "
+ + msg.what
+ + " query URI: "
+ + sanitizeUriToString(args.uri));
+
+ switch (cw.event) {
+ case EVENT_NEW_QUERY:
+ final ContentResolver resolver = mQueryContext.getContentResolver();
+
+ // This should never happen.
+ if (resolver == null) {
+ Log.e(this, "Content Resolver is null!");
+ return;
+ }
+ //start the sql command.
+ Cursor cursor;
+ try {
+ cursor =
+ resolver.query(
+ args.uri,
+ args.projection,
+ args.selection,
+ args.selectionArgs,
+ args.orderBy);
+ // Calling getCount() causes the cursor window to be filled,
+ // which will make the first access on the main thread a lot faster.
+ if (cursor != null) {
+ cursor.getCount();
+ }
+ } catch (Exception e) {
+ Log.e(this, "Exception thrown during handling EVENT_ARG_QUERY", e);
+ cursor = null;
+ }
+
+ args.result = cursor;
+ updateData(msg.arg1, cw, cursor);
+ break;
+
+ // shortcuts to avoid query for recognized numbers.
+ case EVENT_EMERGENCY_NUMBER:
+ case EVENT_VOICEMAIL_NUMBER:
+ case EVENT_ADD_LISTENER:
+ updateData(msg.arg1, cw, (Cursor) args.result);
+ break;
+ default:
+ }
+ Message reply = args.handler.obtainMessage(msg.what);
+ reply.obj = args;
+ reply.arg1 = msg.arg1;
+
+ reply.sendToTarget();
+ }
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/CallerInfoUtils.java b/java/com/android/incallui/CallerInfoUtils.java
new file mode 100644
index 000000000..9f57fba65
--- /dev/null
+++ b/java/com/android/incallui/CallerInfoUtils.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui;
+
+import android.Manifest.permission;
+import android.content.Context;
+import android.content.Loader;
+import android.content.Loader.OnLoadCompleteListener;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.support.v4.content.ContextCompat;
+import android.telecom.PhoneAccount;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import com.android.contacts.common.model.Contact;
+import com.android.contacts.common.model.ContactLoader;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.util.PermissionsUtil;
+import com.android.incallui.call.DialerCall;
+import java.util.Arrays;
+
+/** Utility methods for contact and caller info related functionality */
+public class CallerInfoUtils {
+
+ private static final String TAG = CallerInfoUtils.class.getSimpleName();
+
+ private static final int QUERY_TOKEN = -1;
+
+ public CallerInfoUtils() {}
+
+ /**
+ * This is called to get caller info for a call. This will return a CallerInfo object immediately
+ * based off information in the call, but more information is returned to the
+ * OnQueryCompleteListener (which contains information about the phone number label, user's name,
+ * etc).
+ */
+ public static CallerInfo getCallerInfoForCall(
+ Context context,
+ DialerCall call,
+ Object cookie,
+ CallerInfoAsyncQuery.OnQueryCompleteListener listener) {
+ CallerInfo info = buildCallerInfo(context, call);
+
+ // TODO: Have phoneapp send a Uri when it knows the contact that triggered this call.
+
+ if (info.numberPresentation == TelecomManager.PRESENTATION_ALLOWED) {
+ if (PermissionsUtil.hasContactsPermissions(context)) {
+ // Start the query with the number provided from the call.
+ LogUtil.d(
+ "CallerInfoUtils.getCallerInfoForCall",
+ "Actually starting CallerInfoAsyncQuery.startQuery()...");
+
+ //noinspection MissingPermission
+ CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context, info, listener, cookie);
+ } else {
+ LogUtil.w(
+ "CallerInfoUtils.getCallerInfoForCall",
+ "Dialer doesn't have permission to read contacts."
+ + " Not calling CallerInfoAsyncQuery.startQuery().");
+ }
+ }
+ return info;
+ }
+
+ public static CallerInfo buildCallerInfo(Context context, DialerCall call) {
+ CallerInfo info = new CallerInfo();
+
+ // Store CNAP information retrieved from the Connection (we want to do this
+ // here regardless of whether the number is empty or not).
+ info.cnapName = call.getCnapName();
+ info.name = info.cnapName;
+ info.numberPresentation = call.getNumberPresentation();
+ info.namePresentation = call.getCnapNamePresentation();
+ info.callSubject = call.getCallSubject();
+
+ String number = call.getNumber();
+ if (!TextUtils.isEmpty(number)) {
+ // Don't split it if it's a SIP number.
+ if (!PhoneNumberHelper.isUriNumber(number)) {
+ final String[] numbers = number.split("&");
+ number = numbers[0];
+ if (numbers.length > 1) {
+ info.forwardingNumber = numbers[1];
+ }
+ number = modifyForSpecialCnapCases(context, info, number, info.numberPresentation);
+ }
+ info.phoneNumber = number;
+ }
+
+ // Because the InCallUI is immediately launched before the call is connected, occasionally
+ // a voicemail call will be passed to InCallUI as a "voicemail:" URI without a number.
+ // This call should still be handled as a voicemail call.
+ if ((call.getHandle() != null
+ && PhoneAccount.SCHEME_VOICEMAIL.equals(call.getHandle().getScheme()))
+ || isVoiceMailNumber(context, call)) {
+ info.markAsVoiceMail(context);
+ }
+
+ ContactInfoCache.getInstance(context).maybeInsertCnapInformationIntoCache(context, call, info);
+
+ return info;
+ }
+
+ /**
+ * Creates a new {@link CachedContactInfo} from a {@link CallerInfo}
+ *
+ * @param lookupService the {@link CachedNumberLookupService} used to build a new {@link
+ * CachedContactInfo}
+ * @param {@link CallerInfo} object
+ * @return a CachedContactInfo object created from this CallerInfo
+ * @throws NullPointerException if lookupService or ci are null
+ */
+ public static CachedContactInfo buildCachedContactInfo(
+ CachedNumberLookupService lookupService, CallerInfo ci) {
+ ContactInfo info = new ContactInfo();
+ info.name = ci.name;
+ info.type = ci.numberType;
+ info.label = ci.phoneLabel;
+ info.number = ci.phoneNumber;
+ info.normalizedNumber = ci.normalizedNumber;
+ info.photoUri = ci.contactDisplayPhotoUri;
+ info.userType = ci.userType;
+
+ CachedContactInfo cacheInfo = lookupService.buildCachedContactInfo(info);
+ cacheInfo.setLookupKey(ci.lookupKeyOrNull);
+ return cacheInfo;
+ }
+
+ public static boolean isVoiceMailNumber(Context context, DialerCall call) {
+ if (ContextCompat.checkSelfPermission(context, permission.READ_PHONE_STATE)
+ != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ return TelecomUtil.isVoicemailNumber(context, call.getAccountHandle(), call.getNumber());
+ }
+
+ /**
+ * Handles certain "corner cases" for CNAP. When we receive weird phone numbers from the network
+ * to indicate different number presentations, convert them to expected number and presentation
+ * values within the CallerInfo object.
+ *
+ * @param number number we use to verify if we are in a corner case
+ * @param presentation presentation value used to verify if we are in a corner case
+ * @return the new String that should be used for the phone number
+ */
+ /* package */
+ static String modifyForSpecialCnapCases(
+ Context context, CallerInfo ci, String number, int presentation) {
+ // Obviously we return number if ci == null, but still return number if
+ // number == null, because in these cases the correct string will still be
+ // displayed/logged after this function returns based on the presentation value.
+ if (ci == null || number == null) {
+ return number;
+ }
+
+ LogUtil.d(
+ "CallerInfoUtils.modifyForSpecialCnapCases",
+ "modifyForSpecialCnapCases: initially, number="
+ + toLogSafePhoneNumber(number)
+ + ", presentation="
+ + presentation
+ + " ci "
+ + ci);
+
+ // "ABSENT NUMBER" is a possible value we could get from the network as the
+ // phone number, so if this happens, change it to "Unknown" in the CallerInfo
+ // and fix the presentation to be the same.
+ final String[] absentNumberValues = context.getResources().getStringArray(R.array.absent_num);
+ if (Arrays.asList(absentNumberValues).contains(number)
+ && presentation == TelecomManager.PRESENTATION_ALLOWED) {
+ number = context.getString(R.string.unknown);
+ ci.numberPresentation = TelecomManager.PRESENTATION_UNKNOWN;
+ }
+
+ // Check for other special "corner cases" for CNAP and fix them similarly. Corner
+ // cases only apply if we received an allowed presentation from the network, so check
+ // if we think we have an allowed presentation, or if the CallerInfo presentation doesn't
+ // match the presentation passed in for verification (meaning we changed it previously
+ // because it's a corner case and we're being called from a different entry point).
+ if (ci.numberPresentation == TelecomManager.PRESENTATION_ALLOWED
+ || (ci.numberPresentation != presentation
+ && presentation == TelecomManager.PRESENTATION_ALLOWED)) {
+ // For all special strings, change number & numberPrentation.
+ if (isCnapSpecialCaseRestricted(number)) {
+ number = PhoneNumberHelper.getDisplayNameForRestrictedNumber(context).toString();
+ ci.numberPresentation = TelecomManager.PRESENTATION_RESTRICTED;
+ } else if (isCnapSpecialCaseUnknown(number)) {
+ number = context.getString(R.string.unknown);
+ ci.numberPresentation = TelecomManager.PRESENTATION_UNKNOWN;
+ }
+ LogUtil.d(
+ "CallerInfoUtils.modifyForSpecialCnapCases",
+ "SpecialCnap: number="
+ + toLogSafePhoneNumber(number)
+ + "; presentation now="
+ + ci.numberPresentation);
+ }
+ LogUtil.d(
+ "CallerInfoUtils.modifyForSpecialCnapCases",
+ "returning number string=" + toLogSafePhoneNumber(number));
+ return number;
+ }
+
+ private static boolean isCnapSpecialCaseRestricted(String n) {
+ return n.equals("PRIVATE") || n.equals("P") || n.equals("RES") || n.equals("PRIVATENUMBER");
+ }
+
+ private static boolean isCnapSpecialCaseUnknown(String n) {
+ return n.equals("UNAVAILABLE") || n.equals("UNKNOWN") || n.equals("UNA") || n.equals("U");
+ }
+
+ /* package */
+ static String toLogSafePhoneNumber(String number) {
+ // For unknown number, log empty string.
+ if (number == null) {
+ return "";
+ }
+
+ // Todo: Figure out an equivalent for VDBG
+ if (false) {
+ // When VDBG is true we emit PII.
+ return number;
+ }
+
+ // Do exactly same thing as Uri#toSafeString() does, which will enable us to compare
+ // sanitized phone numbers.
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < number.length(); i++) {
+ char c = number.charAt(i);
+ if (c == '-' || c == '@' || c == '.' || c == '&') {
+ builder.append(c);
+ } else {
+ builder.append('x');
+ }
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Send a notification using a {@link ContactLoader} to inform the sync adapter that we are
+ * viewing a particular contact, so that it can download the high-res photo.
+ */
+ public static void sendViewNotification(Context context, Uri contactUri) {
+ final ContactLoader loader =
+ new ContactLoader(context, contactUri, true /* postViewNotification */);
+ loader.registerListener(
+ 0,
+ new OnLoadCompleteListener<Contact>() {
+ @Override
+ public void onLoadComplete(Loader<Contact> loader, Contact contact) {
+ try {
+ loader.reset();
+ } catch (RuntimeException e) {
+ LogUtil.e("CallerInfoUtils.onLoadComplete", "Error resetting loader", e);
+ }
+ }
+ });
+ loader.startLoading();
+ }
+}
diff --git a/java/com/android/incallui/ConferenceManagerFragment.java b/java/com/android/incallui/ConferenceManagerFragment.java
new file mode 100644
index 000000000..8696bb8ec
--- /dev/null
+++ b/java/com/android/incallui/ConferenceManagerFragment.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.ScreenEvent;
+import com.android.incallui.ConferenceManagerPresenter.ConferenceManagerUi;
+import com.android.incallui.baseui.BaseFragment;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import java.util.List;
+
+/** Fragment that allows the user to manage a conference call. */
+public class ConferenceManagerFragment
+ extends BaseFragment<ConferenceManagerPresenter, ConferenceManagerUi>
+ implements ConferenceManagerPresenter.ConferenceManagerUi {
+
+ private ListView mConferenceParticipantList;
+ private ContactPhotoManager mContactPhotoManager;
+ private ConferenceParticipantListAdapter mConferenceParticipantListAdapter;
+
+ @Override
+ public ConferenceManagerPresenter createPresenter() {
+ return new ConferenceManagerPresenter();
+ }
+
+ @Override
+ public ConferenceManagerPresenter.ConferenceManagerUi getUi() {
+ return this;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ Logger.get(getContext()).logScreenView(ScreenEvent.Type.CONFERENCE_MANAGEMENT, getActivity());
+ }
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View parent = inflater.inflate(R.layout.conference_manager_fragment, container, false);
+
+ mConferenceParticipantList = (ListView) parent.findViewById(R.id.participantList);
+ mContactPhotoManager = ContactPhotoManager.getInstance(getActivity().getApplicationContext());
+
+ return parent;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ final CallList calls = CallList.getInstance();
+ getPresenter().init(calls);
+ // Request focus on the list of participants for accessibility purposes. This ensures
+ // that once the list of participants is shown, the first participant is announced.
+ mConferenceParticipantList.requestFocus();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public boolean isFragmentVisible() {
+ return isVisible();
+ }
+
+ @Override
+ public void update(List<DialerCall> participants, boolean parentCanSeparate) {
+ if (mConferenceParticipantListAdapter == null) {
+ mConferenceParticipantListAdapter =
+ new ConferenceParticipantListAdapter(mConferenceParticipantList, mContactPhotoManager);
+
+ mConferenceParticipantList.setAdapter(mConferenceParticipantListAdapter);
+ }
+ mConferenceParticipantListAdapter.updateParticipants(participants, parentCanSeparate);
+ }
+
+ @Override
+ public void refreshCall(DialerCall call) {
+ mConferenceParticipantListAdapter.refreshCall(call);
+ }
+}
diff --git a/java/com/android/incallui/ConferenceManagerPresenter.java b/java/com/android/incallui/ConferenceManagerPresenter.java
new file mode 100644
index 000000000..226741dcd
--- /dev/null
+++ b/java/com/android/incallui/ConferenceManagerPresenter.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui;
+
+import com.android.incallui.ConferenceManagerPresenter.ConferenceManagerUi;
+import com.android.incallui.InCallPresenter.InCallDetailsListener;
+import com.android.incallui.InCallPresenter.InCallState;
+import com.android.incallui.InCallPresenter.InCallStateListener;
+import com.android.incallui.InCallPresenter.IncomingCallListener;
+import com.android.incallui.baseui.Presenter;
+import com.android.incallui.baseui.Ui;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Logic for call buttons. */
+public class ConferenceManagerPresenter extends Presenter<ConferenceManagerUi>
+ implements InCallStateListener, InCallDetailsListener, IncomingCallListener {
+
+ @Override
+ public void onUiReady(ConferenceManagerUi ui) {
+ super.onUiReady(ui);
+
+ // register for call state changes last
+ InCallPresenter.getInstance().addListener(this);
+ InCallPresenter.getInstance().addIncomingCallListener(this);
+ }
+
+ @Override
+ public void onUiUnready(ConferenceManagerUi ui) {
+ super.onUiUnready(ui);
+
+ InCallPresenter.getInstance().removeListener(this);
+ InCallPresenter.getInstance().removeIncomingCallListener(this);
+ }
+
+ @Override
+ public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
+ if (getUi().isFragmentVisible()) {
+ Log.v(this, "onStateChange" + newState);
+ if (newState == InCallState.INCALL) {
+ final DialerCall call = callList.getActiveOrBackgroundCall();
+ if (call != null && call.isConferenceCall()) {
+ Log.v(
+ this, "Number of existing calls is " + String.valueOf(call.getChildCallIds().size()));
+ update(callList);
+ } else {
+ InCallPresenter.getInstance().showConferenceCallManager(false);
+ }
+ } else {
+ InCallPresenter.getInstance().showConferenceCallManager(false);
+ }
+ }
+ }
+
+ @Override
+ public void onDetailsChanged(DialerCall call, android.telecom.Call.Details details) {
+ boolean canDisconnect =
+ details.can(android.telecom.Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE);
+ boolean canSeparate =
+ details.can(android.telecom.Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE);
+
+ if (call.can(android.telecom.Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE)
+ != canDisconnect
+ || call.can(android.telecom.Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE)
+ != canSeparate) {
+ getUi().refreshCall(call);
+ }
+
+ if (!details.can(android.telecom.Call.Details.CAPABILITY_MANAGE_CONFERENCE)) {
+ InCallPresenter.getInstance().showConferenceCallManager(false);
+ }
+ }
+
+ @Override
+ public void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call) {
+ // When incoming call exists, set conference ui invisible.
+ if (getUi().isFragmentVisible()) {
+ Log.d(this, "onIncomingCall()... Conference ui is showing, hide it.");
+ InCallPresenter.getInstance().showConferenceCallManager(false);
+ }
+ }
+
+ public void init(CallList callList) {
+ update(callList);
+ }
+
+ /**
+ * Updates the conference participant adapter.
+ *
+ * @param callList The callList.
+ */
+ private void update(CallList callList) {
+ // callList is non null, but getActiveOrBackgroundCall() may return null
+ final DialerCall currentCall = callList.getActiveOrBackgroundCall();
+ if (currentCall == null) {
+ return;
+ }
+
+ ArrayList<DialerCall> calls = new ArrayList<>(currentCall.getChildCallIds().size());
+ for (String callerId : currentCall.getChildCallIds()) {
+ calls.add(callList.getCallById(callerId));
+ }
+
+ Log.d(this, "Number of calls is " + String.valueOf(calls.size()));
+
+ // Users can split out a call from the conference call if either the active call or the
+ // holding call is empty. If both are filled, users can not split out another call.
+ final boolean hasActiveCall = (callList.getActiveCall() != null);
+ final boolean hasHoldingCall = (callList.getBackgroundCall() != null);
+ boolean canSeparate = !(hasActiveCall && hasHoldingCall);
+
+ getUi().update(calls, canSeparate);
+ }
+
+ public interface ConferenceManagerUi extends Ui {
+
+ boolean isFragmentVisible();
+
+ void update(List<DialerCall> participants, boolean parentCanSeparate);
+
+ void refreshCall(DialerCall call);
+ }
+}
diff --git a/java/com/android/incallui/ConferenceParticipantListAdapter.java b/java/com/android/incallui/ConferenceParticipantListAdapter.java
new file mode 100644
index 000000000..72c0fcd20
--- /dev/null
+++ b/java/com/android/incallui/ConferenceParticipantListAdapter.java
@@ -0,0 +1,523 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui;
+
+import android.content.Context;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import android.support.v4.util.ArrayMap;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.ContactInfoCache.ContactCacheEntry;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/** Adapter for a ListView containing conference call participant information. */
+public class ConferenceParticipantListAdapter extends BaseAdapter {
+
+ /** The ListView containing the participant information. */
+ private final ListView mListView;
+ /** Hashmap to make accessing participant info by call Id faster. */
+ private final Map<String, ParticipantInfo> mParticipantsByCallId = new ArrayMap<>();
+ /** ContactsPreferences used to lookup displayName preferences */
+ @Nullable private final ContactsPreferences mContactsPreferences;
+ /** Contact photo manager to retrieve cached contact photo information. */
+ private final ContactPhotoManager mContactPhotoManager;
+ /** Listener used to handle tap of the "disconnect' button for a participant. */
+ private View.OnClickListener mDisconnectListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ DialerCall call = getCallFromView(view);
+ LogUtil.i(
+ "ConferenceParticipantListAdapter.mDisconnectListener.onClick", "call: " + call);
+ if (call != null) {
+ call.disconnect();
+ }
+ }
+ };
+ /** Listener used to handle tap of the "separate' button for a participant. */
+ private View.OnClickListener mSeparateListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ DialerCall call = getCallFromView(view);
+ LogUtil.i("ConferenceParticipantListAdapter.mSeparateListener.onClick", "call: " + call);
+ if (call != null) {
+ call.splitFromConference();
+ }
+ }
+ };
+ /** The conference participants to show in the ListView. */
+ private List<ParticipantInfo> mConferenceParticipants = new ArrayList<>();
+ /** {@code True} if the conference parent supports separating calls from the conference. */
+ private boolean mParentCanSeparate;
+
+ /**
+ * Creates an instance of the ConferenceParticipantListAdapter.
+ *
+ * @param listView The listview.
+ * @param contactPhotoManager The contact photo manager, used to load contact photos.
+ */
+ public ConferenceParticipantListAdapter(
+ ListView listView, ContactPhotoManager contactPhotoManager) {
+
+ mListView = listView;
+ mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(getContext());
+ mContactPhotoManager = contactPhotoManager;
+ }
+
+ /**
+ * Updates the adapter with the new conference participant information provided.
+ *
+ * @param conferenceParticipants The list of conference participants.
+ * @param parentCanSeparate {@code True} if the parent supports separating calls from the
+ * conference.
+ */
+ public void updateParticipants(
+ List<DialerCall> conferenceParticipants, boolean parentCanSeparate) {
+ if (mContactsPreferences != null) {
+ mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
+ mContactsPreferences.refreshValue(ContactsPreferences.SORT_ORDER_KEY);
+ }
+ mParentCanSeparate = parentCanSeparate;
+ updateParticipantInfo(conferenceParticipants);
+ }
+
+ /**
+ * Determines the number of participants in the conference.
+ *
+ * @return The number of participants.
+ */
+ @Override
+ public int getCount() {
+ return mConferenceParticipants.size();
+ }
+
+ /**
+ * Retrieves an item from the list of participants.
+ *
+ * @param position Position of the item whose data we want within the adapter's data set.
+ * @return The {@link ParticipantInfo}.
+ */
+ @Override
+ public Object getItem(int position) {
+ return mConferenceParticipants.get(position);
+ }
+
+ /**
+ * Retreives the adapter-specific item id for an item at a specified position.
+ *
+ * @param position The position of the item within the adapter's data set whose row id we want.
+ * @return The item id.
+ */
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ /**
+ * Refreshes call information for the call passed in.
+ *
+ * @param call The new call information.
+ */
+ public void refreshCall(DialerCall call) {
+ String callId = call.getId();
+
+ if (mParticipantsByCallId.containsKey(callId)) {
+ ParticipantInfo participantInfo = mParticipantsByCallId.get(callId);
+ participantInfo.setCall(call);
+ refreshView(callId);
+ }
+ }
+
+ private Context getContext() {
+ return mListView.getContext();
+ }
+
+ /**
+ * Attempts to refresh the view for the specified call ID. This ensures the contact info and photo
+ * loaded from cache are updated.
+ *
+ * @param callId The call id.
+ */
+ private void refreshView(String callId) {
+ int first = mListView.getFirstVisiblePosition();
+ int last = mListView.getLastVisiblePosition();
+
+ for (int position = 0; position <= last - first; position++) {
+ View view = mListView.getChildAt(position);
+ String rowCallId = (String) view.getTag();
+ if (rowCallId.equals(callId)) {
+ getView(position + first, view, mListView);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Creates or populates an existing conference participant row.
+ *
+ * @param position The position of the item within the adapter's data set of the item whose view
+ * we want.
+ * @param convertView The old view to reuse, if possible.
+ * @param parent The parent that this view will eventually be attached to
+ * @return The populated view.
+ */
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ // Make sure we have a valid convertView to start with
+ final View result =
+ convertView == null
+ ? LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.caller_in_conference, parent, false)
+ : convertView;
+
+ ParticipantInfo participantInfo = mConferenceParticipants.get(position);
+ DialerCall call = participantInfo.getCall();
+ ContactCacheEntry contactCache = participantInfo.getContactCacheEntry();
+
+ final ContactInfoCache cache = ContactInfoCache.getInstance(getContext());
+
+ // If a cache lookup has not yet been performed to retrieve the contact information and
+ // photo, do it now.
+ if (!participantInfo.isCacheLookupComplete()) {
+ cache.findInfo(
+ participantInfo.getCall(),
+ participantInfo.getCall().getState() == DialerCall.State.INCOMING,
+ new ContactLookupCallback(this));
+ }
+
+ boolean thisRowCanSeparate =
+ mParentCanSeparate
+ && call.can(android.telecom.Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE);
+ boolean thisRowCanDisconnect =
+ call.can(android.telecom.Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE);
+
+ setCallerInfoForRow(
+ result,
+ contactCache.namePrimary,
+ ContactDisplayUtils.getPreferredDisplayName(
+ contactCache.namePrimary, contactCache.nameAlternative, mContactsPreferences),
+ contactCache.number,
+ contactCache.label,
+ contactCache.lookupKey,
+ contactCache.displayPhotoUri,
+ thisRowCanSeparate,
+ thisRowCanDisconnect);
+
+ // Tag the row in the conference participant list with the call id to make it easier to
+ // find calls when contact cache information is loaded.
+ result.setTag(call.getId());
+
+ return result;
+ }
+
+ /**
+ * Replaces the contact info for a participant and triggers a refresh of the UI.
+ *
+ * @param callId The call id.
+ * @param entry The new contact info.
+ */
+ /* package */ void updateContactInfo(String callId, ContactCacheEntry entry) {
+ if (mParticipantsByCallId.containsKey(callId)) {
+ ParticipantInfo participantInfo = mParticipantsByCallId.get(callId);
+ participantInfo.setContactCacheEntry(entry);
+ participantInfo.setCacheLookupComplete(true);
+ refreshView(callId);
+ }
+ }
+
+ /**
+ * Sets the caller information for a row in the conference participant list.
+ *
+ * @param view The view to set the details on.
+ * @param callerName The participant's name.
+ * @param callerNumber The participant's phone number.
+ * @param callerNumberType The participant's phone number typ.e
+ * @param lookupKey The lookup key for the participant (for photo lookup).
+ * @param photoUri The URI of the contact photo.
+ * @param thisRowCanSeparate {@code True} if this participant can separate from the conference.
+ * @param thisRowCanDisconnect {@code True} if this participant can be disconnected.
+ */
+ private void setCallerInfoForRow(
+ View view,
+ String callerName,
+ String preferredName,
+ String callerNumber,
+ String callerNumberType,
+ String lookupKey,
+ Uri photoUri,
+ boolean thisRowCanSeparate,
+ boolean thisRowCanDisconnect) {
+
+ final ImageView photoView = (ImageView) view.findViewById(R.id.callerPhoto);
+ final TextView nameTextView = (TextView) view.findViewById(R.id.conferenceCallerName);
+ final TextView numberTextView = (TextView) view.findViewById(R.id.conferenceCallerNumber);
+ final TextView numberTypeTextView =
+ (TextView) view.findViewById(R.id.conferenceCallerNumberType);
+ final View endButton = view.findViewById(R.id.conferenceCallerDisconnect);
+ final View separateButton = view.findViewById(R.id.conferenceCallerSeparate);
+
+ endButton.setVisibility(thisRowCanDisconnect ? View.VISIBLE : View.GONE);
+ if (thisRowCanDisconnect) {
+ endButton.setOnClickListener(mDisconnectListener);
+ } else {
+ endButton.setOnClickListener(null);
+ }
+
+ separateButton.setVisibility(thisRowCanSeparate ? View.VISIBLE : View.GONE);
+ if (thisRowCanSeparate) {
+ separateButton.setOnClickListener(mSeparateListener);
+ } else {
+ separateButton.setOnClickListener(null);
+ }
+
+ DefaultImageRequest imageRequest =
+ (photoUri != null)
+ ? null
+ : new DefaultImageRequest(callerName, lookupKey, true /* isCircularPhoto */);
+
+ mContactPhotoManager.loadDirectoryPhoto(photoView, photoUri, false, true, imageRequest);
+
+ // set the caller name
+ nameTextView.setText(preferredName);
+
+ // set the caller number in subscript, or make the field disappear.
+ if (TextUtils.isEmpty(callerNumber)) {
+ numberTextView.setVisibility(View.GONE);
+ numberTypeTextView.setVisibility(View.GONE);
+ } else {
+ numberTextView.setVisibility(View.VISIBLE);
+ numberTextView.setText(
+ PhoneNumberUtilsCompat.createTtsSpannable(
+ BidiFormatter.getInstance().unicodeWrap(callerNumber, TextDirectionHeuristics.LTR)));
+ numberTypeTextView.setVisibility(View.VISIBLE);
+ numberTypeTextView.setText(callerNumberType);
+ }
+ }
+
+ /**
+ * Updates the participant info list which is bound to the ListView. Stores the call and contact
+ * info for all entries. The list is sorted alphabetically by participant name.
+ *
+ * @param conferenceParticipants The calls which make up the conference participants.
+ */
+ private void updateParticipantInfo(List<DialerCall> conferenceParticipants) {
+ final ContactInfoCache cache = ContactInfoCache.getInstance(getContext());
+ boolean newParticipantAdded = false;
+ Set<String> newCallIds = new ArraySet<>(conferenceParticipants.size());
+
+ // Update or add conference participant info.
+ for (DialerCall call : conferenceParticipants) {
+ String callId = call.getId();
+ newCallIds.add(callId);
+ ContactCacheEntry contactCache = cache.getInfo(callId);
+ if (contactCache == null) {
+ contactCache =
+ ContactInfoCache.buildCacheEntryFromCall(
+ getContext(), call, call.getState() == DialerCall.State.INCOMING);
+ }
+
+ if (mParticipantsByCallId.containsKey(callId)) {
+ ParticipantInfo participantInfo = mParticipantsByCallId.get(callId);
+ participantInfo.setCall(call);
+ participantInfo.setContactCacheEntry(contactCache);
+ } else {
+ newParticipantAdded = true;
+ ParticipantInfo participantInfo = new ParticipantInfo(call, contactCache);
+ mConferenceParticipants.add(participantInfo);
+ mParticipantsByCallId.put(call.getId(), participantInfo);
+ }
+ }
+
+ // Remove any participants that no longer exist.
+ Iterator<Map.Entry<String, ParticipantInfo>> it = mParticipantsByCallId.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<String, ParticipantInfo> entry = it.next();
+ String existingCallId = entry.getKey();
+ if (!newCallIds.contains(existingCallId)) {
+ ParticipantInfo existingInfo = entry.getValue();
+ mConferenceParticipants.remove(existingInfo);
+ it.remove();
+ }
+ }
+
+ if (newParticipantAdded) {
+ // Sort the list of participants by contact name.
+ sortParticipantList();
+ }
+ notifyDataSetChanged();
+ }
+
+ /** Sorts the participant list by contact name. */
+ private void sortParticipantList() {
+ Collections.sort(
+ mConferenceParticipants,
+ new Comparator<ParticipantInfo>() {
+ @Override
+ public int compare(ParticipantInfo p1, ParticipantInfo p2) {
+ // Contact names might be null, so replace with empty string.
+ ContactCacheEntry c1 = p1.getContactCacheEntry();
+ String p1Name =
+ ContactDisplayUtils.getPreferredSortName(
+ c1.namePrimary, c1.nameAlternative, mContactsPreferences);
+ p1Name = p1Name != null ? p1Name : "";
+
+ ContactCacheEntry c2 = p2.getContactCacheEntry();
+ String p2Name =
+ ContactDisplayUtils.getPreferredSortName(
+ c2.namePrimary, c2.nameAlternative, mContactsPreferences);
+ p2Name = p2Name != null ? p2Name : "";
+
+ return p1Name.compareToIgnoreCase(p2Name);
+ }
+ });
+ }
+
+ private DialerCall getCallFromView(View view) {
+ View parent = (View) view.getParent();
+ String callId = (String) parent.getTag();
+ return CallList.getInstance().getCallById(callId);
+ }
+
+ /**
+ * Callback class used when making requests to the {@link ContactInfoCache} to resolve contact
+ * info and contact photos for conference participants.
+ */
+ public static class ContactLookupCallback implements ContactInfoCache.ContactInfoCacheCallback {
+
+ private final WeakReference<ConferenceParticipantListAdapter> mListAdapter;
+
+ public ContactLookupCallback(ConferenceParticipantListAdapter listAdapter) {
+ mListAdapter = new WeakReference<>(listAdapter);
+ }
+
+ /**
+ * Called when contact info has been resolved.
+ *
+ * @param callId The call id.
+ * @param entry The new contact information.
+ */
+ @Override
+ public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
+ update(callId, entry);
+ }
+
+ /**
+ * Called when contact photo has been loaded into the cache.
+ *
+ * @param callId The call id.
+ * @param entry The new contact information.
+ */
+ @Override
+ public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
+ update(callId, entry);
+ }
+
+ /**
+ * Updates the contact information for a participant.
+ *
+ * @param callId The call id.
+ * @param entry The new contact information.
+ */
+ private void update(String callId, ContactCacheEntry entry) {
+ ConferenceParticipantListAdapter listAdapter = mListAdapter.get();
+ if (listAdapter != null) {
+ listAdapter.updateContactInfo(callId, entry);
+ }
+ }
+ }
+
+ /**
+ * Internal class which represents a participant. Includes a reference to the {@link DialerCall}
+ * and the corresponding {@link ContactCacheEntry} for the participant.
+ */
+ private static class ParticipantInfo {
+
+ private DialerCall mCall;
+ private ContactCacheEntry mContactCacheEntry;
+ private boolean mCacheLookupComplete = false;
+
+ public ParticipantInfo(DialerCall call, ContactCacheEntry contactCacheEntry) {
+ mCall = call;
+ mContactCacheEntry = contactCacheEntry;
+ }
+
+ public DialerCall getCall() {
+ return mCall;
+ }
+
+ public void setCall(DialerCall call) {
+ mCall = call;
+ }
+
+ public ContactCacheEntry getContactCacheEntry() {
+ return mContactCacheEntry;
+ }
+
+ public void setContactCacheEntry(ContactCacheEntry entry) {
+ mContactCacheEntry = entry;
+ }
+
+ public boolean isCacheLookupComplete() {
+ return mCacheLookupComplete;
+ }
+
+ public void setCacheLookupComplete(boolean cacheLookupComplete) {
+ mCacheLookupComplete = cacheLookupComplete;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof ParticipantInfo) {
+ ParticipantInfo p = (ParticipantInfo) o;
+ return Objects.equals(p.getCall().getId(), mCall.getId());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mCall.getId().hashCode();
+ }
+ }
+}
diff --git a/java/com/android/incallui/ContactInfoCache.java b/java/com/android/incallui/ContactInfoCache.java
new file mode 100644
index 000000000..4d4d94a17
--- /dev/null
+++ b/java/com/android/incallui/ContactInfoCache.java
@@ -0,0 +1,759 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.support.annotation.AnyThread;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import android.support.v4.os.UserManagerCompat;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import com.android.contacts.common.ContactsUtils;
+import com.android.dialer.common.Assert;
+import com.android.dialer.logging.nano.ContactLookupResult;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.phonenumbercache.PhoneNumberCache;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.util.MoreStrings;
+import com.android.incallui.CallerInfoAsyncQuery.OnQueryCompleteListener;
+import com.android.incallui.ContactsAsyncHelper.OnImageLoadCompleteListener;
+import com.android.incallui.bindings.PhoneNumberService;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.incall.protocol.ContactPhotoType;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Class responsible for querying Contact Information for DialerCall objects. Can perform
+ * asynchronous requests to the Contact Provider for information as well as respond synchronously
+ * for any data that it currently has cached from previous queries. This class always gets called
+ * from the UI thread so it does not need thread protection.
+ */
+public class ContactInfoCache implements OnImageLoadCompleteListener {
+
+ private static final String TAG = ContactInfoCache.class.getSimpleName();
+ private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0;
+ private static ContactInfoCache sCache = null;
+ private final Context mContext;
+ private final PhoneNumberService mPhoneNumberService;
+ // Cache info map needs to be thread-safe since it could be modified by both main thread and
+ // worker thread.
+ private final Map<String, ContactCacheEntry> mInfoMap = new ConcurrentHashMap<>();
+ private final Map<String, Set<ContactInfoCacheCallback>> mCallBacks = new ArrayMap<>();
+ private Drawable mDefaultContactPhotoDrawable;
+ private Drawable mConferencePhotoDrawable;
+
+ private ContactInfoCache(Context context) {
+ mContext = context;
+ mPhoneNumberService = Bindings.get(context).newPhoneNumberService(context);
+ }
+
+ public static synchronized ContactInfoCache getInstance(Context mContext) {
+ if (sCache == null) {
+ sCache = new ContactInfoCache(mContext.getApplicationContext());
+ }
+ return sCache;
+ }
+
+ public static ContactCacheEntry buildCacheEntryFromCall(
+ Context context, DialerCall call, boolean isIncoming) {
+ final ContactCacheEntry entry = new ContactCacheEntry();
+
+ // TODO: get rid of caller info.
+ final CallerInfo info = CallerInfoUtils.buildCallerInfo(context, call);
+ ContactInfoCache.populateCacheEntry(
+ context, info, entry, call.getNumberPresentation(), isIncoming);
+ return entry;
+ }
+
+ /** Populate a cache entry from a call (which got converted into a caller info). */
+ public static void populateCacheEntry(
+ @NonNull Context context,
+ @NonNull CallerInfo info,
+ @NonNull ContactCacheEntry cce,
+ int presentation,
+ boolean isIncoming) {
+ Objects.requireNonNull(info);
+ String displayName = null;
+ String displayNumber = null;
+ String displayLocation = null;
+ String label = null;
+ boolean isSipCall = false;
+
+ // It appears that there is a small change in behaviour with the
+ // PhoneUtils' startGetCallerInfo whereby if we query with an
+ // empty number, we will get a valid CallerInfo object, but with
+ // fields that are all null, and the isTemporary boolean input
+ // parameter as true.
+
+ // In the past, we would see a NULL callerinfo object, but this
+ // ends up causing null pointer exceptions elsewhere down the
+ // line in other cases, so we need to make this fix instead. It
+ // appears that this was the ONLY call to PhoneUtils
+ // .getCallerInfo() that relied on a NULL CallerInfo to indicate
+ // an unknown contact.
+
+ // Currently, info.phoneNumber may actually be a SIP address, and
+ // if so, it might sometimes include the "sip:" prefix. That
+ // prefix isn't really useful to the user, though, so strip it off
+ // if present. (For any other URI scheme, though, leave the
+ // prefix alone.)
+ // TODO: It would be cleaner for CallerInfo to explicitly support
+ // SIP addresses instead of overloading the "phoneNumber" field.
+ // Then we could remove this hack, and instead ask the CallerInfo
+ // for a "user visible" form of the SIP address.
+ String number = info.phoneNumber;
+
+ if (!TextUtils.isEmpty(number)) {
+ isSipCall = PhoneNumberHelper.isUriNumber(number);
+ if (number.startsWith("sip:")) {
+ number = number.substring(4);
+ }
+ }
+
+ if (TextUtils.isEmpty(info.name)) {
+ // No valid "name" in the CallerInfo, so fall back to
+ // something else.
+ // (Typically, we promote the phone number up to the "name" slot
+ // onscreen, and possibly display a descriptive string in the
+ // "number" slot.)
+ if (TextUtils.isEmpty(number)) {
+ // No name *or* number! Display a generic "unknown" string
+ // (or potentially some other default based on the presentation.)
+ displayName = getPresentationString(context, presentation, info.callSubject);
+ Log.d(TAG, " ==> no name *or* number! displayName = " + displayName);
+ } else if (presentation != TelecomManager.PRESENTATION_ALLOWED) {
+ // This case should never happen since the network should never send a phone #
+ // AND a restricted presentation. However we leave it here in case of weird
+ // network behavior
+ displayName = getPresentationString(context, presentation, info.callSubject);
+ Log.d(TAG, " ==> presentation not allowed! displayName = " + displayName);
+ } else if (!TextUtils.isEmpty(info.cnapName)) {
+ // No name, but we do have a valid CNAP name, so use that.
+ displayName = info.cnapName;
+ info.name = info.cnapName;
+ displayNumber = PhoneNumberHelper.formatNumber(number, context);
+ Log.d(
+ TAG,
+ " ==> cnapName available: displayName '"
+ + displayName
+ + "', displayNumber '"
+ + displayNumber
+ + "'");
+ } else {
+ // No name; all we have is a number. This is the typical
+ // case when an incoming call doesn't match any contact,
+ // or if you manually dial an outgoing number using the
+ // dialpad.
+ displayNumber = PhoneNumberHelper.formatNumber(number, context);
+
+ // Display a geographical description string if available
+ // (but only for incoming calls.)
+ if (isIncoming) {
+ // TODO (CallerInfoAsyncQuery cleanup): Fix the CallerInfo
+ // query to only do the geoDescription lookup in the first
+ // place for incoming calls.
+ displayLocation = info.geoDescription; // may be null
+ Log.d(TAG, "Geodescrption: " + info.geoDescription);
+ }
+
+ Log.d(
+ TAG,
+ " ==> no name; falling back to number:"
+ + " displayNumber '"
+ + Log.pii(displayNumber)
+ + "', displayLocation '"
+ + displayLocation
+ + "'");
+ }
+ } else {
+ // We do have a valid "name" in the CallerInfo. Display that
+ // in the "name" slot, and the phone number in the "number" slot.
+ if (presentation != TelecomManager.PRESENTATION_ALLOWED) {
+ // This case should never happen since the network should never send a name
+ // AND a restricted presentation. However we leave it here in case of weird
+ // network behavior
+ displayName = getPresentationString(context, presentation, info.callSubject);
+ Log.d(
+ TAG,
+ " ==> valid name, but presentation not allowed!" + " displayName = " + displayName);
+ } else {
+ // Causes cce.namePrimary to be set as info.name below. CallCardPresenter will
+ // later determine whether to use the name or nameAlternative when presenting
+ displayName = info.name;
+ cce.nameAlternative = info.nameAlternative;
+ displayNumber = PhoneNumberHelper.formatNumber(number, context);
+ label = info.phoneLabel;
+ Log.d(
+ TAG,
+ " ==> name is present in CallerInfo: displayName '"
+ + displayName
+ + "', displayNumber '"
+ + displayNumber
+ + "'");
+ }
+ }
+
+ cce.namePrimary = displayName;
+ cce.number = displayNumber;
+ cce.location = displayLocation;
+ cce.label = label;
+ cce.isSipCall = isSipCall;
+ cce.userType = info.userType;
+
+ if (info.contactExists) {
+ cce.contactLookupResult = ContactLookupResult.Type.LOCAL_CONTACT;
+ }
+ }
+
+ /** Gets name strings based on some special presentation modes and the associated custom label. */
+ private static String getPresentationString(
+ Context context, int presentation, String customLabel) {
+ String name = context.getString(R.string.unknown);
+ if (!TextUtils.isEmpty(customLabel)
+ && ((presentation == TelecomManager.PRESENTATION_UNKNOWN)
+ || (presentation == TelecomManager.PRESENTATION_RESTRICTED))) {
+ name = customLabel;
+ return name;
+ } else {
+ if (presentation == TelecomManager.PRESENTATION_RESTRICTED) {
+ name = PhoneNumberHelper.getDisplayNameForRestrictedNumber(context).toString();
+ } else if (presentation == TelecomManager.PRESENTATION_PAYPHONE) {
+ name = context.getString(R.string.payphone);
+ }
+ }
+ return name;
+ }
+
+ public ContactCacheEntry getInfo(String callId) {
+ return mInfoMap.get(callId);
+ }
+
+ public void maybeInsertCnapInformationIntoCache(
+ Context context, final DialerCall call, final CallerInfo info) {
+ final CachedNumberLookupService cachedNumberLookupService =
+ PhoneNumberCache.get(context).getCachedNumberLookupService();
+ if (!UserManagerCompat.isUserUnlocked(context)) {
+ Log.i(TAG, "User locked, not inserting cnap info into cache");
+ return;
+ }
+ if (cachedNumberLookupService == null
+ || TextUtils.isEmpty(info.cnapName)
+ || mInfoMap.get(call.getId()) != null) {
+ return;
+ }
+ final Context applicationContext = context.getApplicationContext();
+ Log.i(TAG, "Found contact with CNAP name - inserting into cache");
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ ContactInfo contactInfo = new ContactInfo();
+ CachedContactInfo cacheInfo = cachedNumberLookupService.buildCachedContactInfo(contactInfo);
+ cacheInfo.setSource(CachedContactInfo.SOURCE_TYPE_CNAP, "CNAP", 0);
+ contactInfo.name = info.cnapName;
+ contactInfo.number = call.getNumber();
+ contactInfo.type = ContactsContract.CommonDataKinds.Phone.TYPE_MAIN;
+ try {
+ final JSONObject contactRows =
+ new JSONObject()
+ .put(
+ Phone.CONTENT_ITEM_TYPE,
+ new JSONObject()
+ .put(Phone.NUMBER, contactInfo.number)
+ .put(Phone.TYPE, Phone.TYPE_MAIN));
+ final String jsonString =
+ new JSONObject()
+ .put(Contacts.DISPLAY_NAME, contactInfo.name)
+ .put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME)
+ .put(Contacts.CONTENT_ITEM_TYPE, contactRows)
+ .toString();
+ cacheInfo.setLookupKey(jsonString);
+ } catch (JSONException e) {
+ Log.w(TAG, "Creation of lookup key failed when caching CNAP information");
+ }
+ cachedNumberLookupService.addContact(applicationContext, cacheInfo);
+ return null;
+ }
+ }.execute();
+ }
+
+ /**
+ * Requests contact data for the DialerCall object passed in. Returns the data through callback.
+ * If callback is null, no response is made, however the query is still performed and cached.
+ *
+ * @param callback The function to call back when the call is found. Can be null.
+ */
+ @MainThread
+ public void findInfo(
+ @NonNull final DialerCall call,
+ final boolean isIncoming,
+ @NonNull ContactInfoCacheCallback callback) {
+ Assert.isMainThread();
+ Objects.requireNonNull(callback);
+
+ final String callId = call.getId();
+ final ContactCacheEntry cacheEntry = mInfoMap.get(callId);
+ Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
+
+ // If we have a previously obtained intermediate result return that now
+ if (cacheEntry != null) {
+ Log.d(
+ TAG,
+ "Contact lookup. In memory cache hit; lookup "
+ + (callBacks == null ? "complete" : "still running"));
+ callback.onContactInfoComplete(callId, cacheEntry);
+ // If no other callbacks are in flight, we're done.
+ if (callBacks == null) {
+ return;
+ }
+ }
+
+ // If the entry already exists, add callback
+ if (callBacks != null) {
+ callBacks.add(callback);
+ return;
+ }
+ Log.d(TAG, "Contact lookup. In memory cache miss; searching provider.");
+ // New lookup
+ callBacks = new ArraySet<>();
+ callBacks.add(callback);
+ mCallBacks.put(callId, callBacks);
+
+ /**
+ * Performs a query for caller information. Save any immediate data we get from the query. An
+ * asynchronous query may also be made for any data that we do not already have. Some queries,
+ * such as those for voicemail and emergency call information, will not perform an additional
+ * asynchronous query.
+ */
+ final CallerInfo callerInfo =
+ CallerInfoUtils.getCallerInfoForCall(
+ mContext,
+ call,
+ new DialerCallCookieWrapper(callId, call.getNumberPresentation()),
+ new FindInfoCallback(isIncoming));
+
+ updateCallerInfoInCacheOnAnyThread(
+ callId, call.getNumberPresentation(), callerInfo, isIncoming, false);
+ sendInfoNotifications(callId, mInfoMap.get(callId));
+ }
+
+ @AnyThread
+ private void updateCallerInfoInCacheOnAnyThread(
+ String callId,
+ int numberPresentation,
+ CallerInfo callerInfo,
+ boolean isIncoming,
+ boolean didLocalLookup) {
+ int presentationMode = numberPresentation;
+ if (callerInfo.contactExists
+ || callerInfo.isEmergencyNumber()
+ || callerInfo.isVoiceMailNumber()) {
+ presentationMode = TelecomManager.PRESENTATION_ALLOWED;
+ }
+
+ synchronized (mInfoMap) {
+ ContactCacheEntry cacheEntry = mInfoMap.get(callId);
+ // Ensure we always have a cacheEntry. Replace the existing entry if
+ // it has no name or if we found a local contact.
+ if (cacheEntry == null
+ || TextUtils.isEmpty(cacheEntry.namePrimary)
+ || callerInfo.contactExists) {
+ cacheEntry = buildEntry(mContext, callerInfo, presentationMode, isIncoming);
+ mInfoMap.put(callId, cacheEntry);
+ }
+ if (didLocalLookup) {
+ // Before issuing a request for more data from other services, we only check that the
+ // contact wasn't found in the local DB. We don't check the if the cache entry already
+ // has a name because we allow overriding cnap data with data from other services.
+ if (!callerInfo.contactExists && mPhoneNumberService != null) {
+ Log.d(TAG, "Contact lookup. Local contacts miss, checking remote");
+ final PhoneNumberServiceListener listener = new PhoneNumberServiceListener(callId);
+ mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener, isIncoming);
+ } else if (cacheEntry.displayPhotoUri != null) {
+ Log.d(TAG, "Contact lookup. Local contact found, starting image load");
+ // Load the image with a callback to update the image state.
+ // When the load is finished, onImageLoadComplete() will be called.
+ cacheEntry.hasPhotoToLoad = true;
+ ContactsAsyncHelper.startObtainPhotoAsync(
+ TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
+ mContext,
+ cacheEntry.displayPhotoUri,
+ ContactInfoCache.this,
+ callId);
+ }
+ }
+ }
+ }
+
+ /**
+ * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface. Update contact photo
+ * when image is loaded in worker thread.
+ */
+ @WorkerThread
+ @Override
+ public void onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
+ Assert.isWorkerThread();
+ loadImage(photo, photoIcon, cookie);
+ }
+
+ private void loadImage(Drawable photo, Bitmap photoIcon, Object cookie) {
+ Log.d(this, "Image load complete with context: ", mContext);
+ // TODO: may be nice to update the image view again once the newer one
+ // is available on contacts database.
+ String callId = (String) cookie;
+ ContactCacheEntry entry = mInfoMap.get(callId);
+
+ if (entry == null) {
+ Log.e(this, "Image Load received for empty search entry.");
+ clearCallbacks(callId);
+ return;
+ }
+
+ Log.d(this, "setting photo for entry: ", entry);
+
+ // Conference call icons are being handled in CallCardPresenter.
+ if (photo != null) {
+ Log.v(this, "direct drawable: ", photo);
+ entry.photo = photo;
+ entry.photoType = ContactPhotoType.CONTACT;
+ } else if (photoIcon != null) {
+ Log.v(this, "photo icon: ", photoIcon);
+ entry.photo = new BitmapDrawable(mContext.getResources(), photoIcon);
+ entry.photoType = ContactPhotoType.CONTACT;
+ } else {
+ Log.v(this, "unknown photo");
+ entry.photo = null;
+ entry.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
+ }
+ }
+
+ /**
+ * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface. make sure that the
+ * call state is reflected after the image is loaded.
+ */
+ @MainThread
+ @Override
+ public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
+ Assert.isMainThread();
+ String callId = (String) cookie;
+ ContactCacheEntry entry = mInfoMap.get(callId);
+ sendImageNotifications(callId, entry);
+
+ clearCallbacks(callId);
+ }
+
+ /** Blows away the stored cache values. */
+ public void clearCache() {
+ mInfoMap.clear();
+ mCallBacks.clear();
+ }
+
+ private ContactCacheEntry buildEntry(
+ Context context, CallerInfo info, int presentation, boolean isIncoming) {
+ final ContactCacheEntry cce = new ContactCacheEntry();
+ populateCacheEntry(context, info, cce, presentation, isIncoming);
+
+ // This will only be true for emergency numbers
+ if (info.photoResource != 0) {
+ cce.photo = context.getResources().getDrawable(info.photoResource);
+ } else if (info.isCachedPhotoCurrent) {
+ if (info.cachedPhoto != null) {
+ cce.photo = info.cachedPhoto;
+ cce.photoType = ContactPhotoType.CONTACT;
+ } else {
+ cce.photo = getDefaultContactPhotoDrawable();
+ cce.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
+ }
+ } else if (info.contactDisplayPhotoUri == null) {
+ cce.photo = getDefaultContactPhotoDrawable();
+ cce.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
+ } else {
+ cce.displayPhotoUri = info.contactDisplayPhotoUri;
+ cce.photo = null;
+ }
+
+ // Support any contact id in N because QuickContacts in N starts supporting enterprise
+ // contact id
+ if (info.lookupKeyOrNull != null
+ && (VERSION.SDK_INT >= VERSION_CODES.N || info.contactIdOrZero != 0)) {
+ cce.lookupUri = Contacts.getLookupUri(info.contactIdOrZero, info.lookupKeyOrNull);
+ } else {
+ Log.v(TAG, "lookup key is null or contact ID is 0 on M. Don't create a lookup uri.");
+ cce.lookupUri = null;
+ }
+
+ cce.lookupKey = info.lookupKeyOrNull;
+ cce.contactRingtoneUri = info.contactRingtoneUri;
+ if (cce.contactRingtoneUri == null || Uri.EMPTY.equals(cce.contactRingtoneUri)) {
+ cce.contactRingtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
+ }
+
+ return cce;
+ }
+
+ /** Sends the updated information to call the callbacks for the entry. */
+ private void sendInfoNotifications(String callId, ContactCacheEntry entry) {
+ final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
+ if (callBacks != null) {
+ for (ContactInfoCacheCallback callBack : callBacks) {
+ callBack.onContactInfoComplete(callId, entry);
+ }
+ }
+ }
+
+ private void sendImageNotifications(String callId, ContactCacheEntry entry) {
+ final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
+ if (callBacks != null && entry.photo != null) {
+ for (ContactInfoCacheCallback callBack : callBacks) {
+ callBack.onImageLoadComplete(callId, entry);
+ }
+ }
+ }
+
+ private void clearCallbacks(String callId) {
+ mCallBacks.remove(callId);
+ }
+
+ public Drawable getDefaultContactPhotoDrawable() {
+ if (mDefaultContactPhotoDrawable == null) {
+ mDefaultContactPhotoDrawable =
+ mContext.getResources().getDrawable(R.drawable.img_no_image_automirrored);
+ }
+ return mDefaultContactPhotoDrawable;
+ }
+
+ public Drawable getConferenceDrawable() {
+ if (mConferencePhotoDrawable == null) {
+ mConferencePhotoDrawable =
+ mContext.getResources().getDrawable(R.drawable.img_conference_automirrored);
+ }
+ return mConferencePhotoDrawable;
+ }
+
+ /** Callback interface for the contact query. */
+ public interface ContactInfoCacheCallback {
+
+ void onContactInfoComplete(String callId, ContactCacheEntry entry);
+
+ void onImageLoadComplete(String callId, ContactCacheEntry entry);
+ }
+
+ /** This is cached contact info, which should be the ONLY info used by UI. */
+ public static class ContactCacheEntry {
+
+ public String namePrimary;
+ public String nameAlternative;
+ public String number;
+ public String location;
+ public String label;
+ public Drawable photo;
+ @ContactPhotoType public int photoType;
+ public boolean isSipCall;
+ // Note in cache entry whether this is a pending async loading action to know whether to
+ // wait for its callback or not.
+ public boolean hasPhotoToLoad;
+ /** This will be used for the "view" notification. */
+ public Uri contactUri;
+ /** Either a display photo or a thumbnail URI. */
+ public Uri displayPhotoUri;
+
+ public Uri lookupUri; // Sent to NotificationMananger
+ public String lookupKey;
+ public int contactLookupResult = ContactLookupResult.Type.NOT_FOUND;
+ public long userType = ContactsUtils.USER_TYPE_CURRENT;
+ public Uri contactRingtoneUri;
+
+ @Override
+ public String toString() {
+ return "ContactCacheEntry{"
+ + "name='"
+ + MoreStrings.toSafeString(namePrimary)
+ + '\''
+ + ", nameAlternative='"
+ + MoreStrings.toSafeString(nameAlternative)
+ + '\''
+ + ", number='"
+ + MoreStrings.toSafeString(number)
+ + '\''
+ + ", location='"
+ + MoreStrings.toSafeString(location)
+ + '\''
+ + ", label='"
+ + label
+ + '\''
+ + ", photo="
+ + photo
+ + ", isSipCall="
+ + isSipCall
+ + ", contactUri="
+ + contactUri
+ + ", displayPhotoUri="
+ + displayPhotoUri
+ + ", contactLookupResult="
+ + contactLookupResult
+ + ", userType="
+ + userType
+ + ", contactRingtoneUri="
+ + contactRingtoneUri
+ + '}';
+ }
+ }
+
+ private static final class DialerCallCookieWrapper {
+ public final String callId;
+ public final int numberPresentation;
+
+ public DialerCallCookieWrapper(String callId, int numberPresentation) {
+ this.callId = callId;
+ this.numberPresentation = numberPresentation;
+ }
+ }
+
+ private class FindInfoCallback implements OnQueryCompleteListener {
+
+ private final boolean mIsIncoming;
+
+ public FindInfoCallback(boolean isIncoming) {
+ mIsIncoming = isIncoming;
+ }
+
+ @Override
+ public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
+ Assert.isWorkerThread();
+ DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie;
+ updateCallerInfoInCacheOnAnyThread(cw.callId, cw.numberPresentation, ci, mIsIncoming, true);
+ }
+
+ @Override
+ public void onQueryComplete(int token, Object cookie, CallerInfo callerInfo) {
+ Assert.isMainThread();
+ DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie;
+ String callId = cw.callId;
+ ContactCacheEntry cacheEntry = mInfoMap.get(callId);
+ // This may happen only when InCallPresenter attempt to cleanup.
+ if (cacheEntry == null) {
+ Log.w(TAG, "Contact lookup done, but cache entry is not found.");
+ clearCallbacks(callId);
+ return;
+ }
+ sendInfoNotifications(callId, cacheEntry);
+ if (!cacheEntry.hasPhotoToLoad) {
+ if (callerInfo.contactExists) {
+ Log.d(TAG, "Contact lookup done. Local contact found, no image.");
+ } else {
+ Log.d(
+ TAG,
+ "Contact lookup done. Local contact not found and"
+ + " no remote lookup service available.");
+ }
+ clearCallbacks(callId);
+ }
+ }
+ }
+
+ class PhoneNumberServiceListener
+ implements PhoneNumberService.NumberLookupListener, PhoneNumberService.ImageLookupListener {
+
+ private final String mCallId;
+
+ PhoneNumberServiceListener(String callId) {
+ mCallId = callId;
+ }
+
+ @Override
+ public void onPhoneNumberInfoComplete(final PhoneNumberService.PhoneNumberInfo info) {
+ // If we got a miss, this is the end of the lookup pipeline,
+ // so clear the callbacks and return.
+ if (info == null) {
+ Log.d(TAG, "Contact lookup done. Remote contact not found.");
+ clearCallbacks(mCallId);
+ return;
+ }
+
+ ContactCacheEntry entry = new ContactCacheEntry();
+ entry.namePrimary = info.getDisplayName();
+ entry.number = info.getNumber();
+ entry.contactLookupResult = info.getLookupSource();
+ final int type = info.getPhoneType();
+ final String label = info.getPhoneLabel();
+ if (type == Phone.TYPE_CUSTOM) {
+ entry.label = label;
+ } else {
+ final CharSequence typeStr = Phone.getTypeLabel(mContext.getResources(), type, label);
+ entry.label = typeStr == null ? null : typeStr.toString();
+ }
+ synchronized (mInfoMap) {
+ final ContactCacheEntry oldEntry = mInfoMap.get(mCallId);
+ if (oldEntry != null) {
+ // Location is only obtained from local lookup so persist
+ // the value for remote lookups. Once we have a name this
+ // field is no longer used; it is persisted here in case
+ // the UI is ever changed to use it.
+ entry.location = oldEntry.location;
+ // Contact specific ringtone is obtained from local lookup.
+ entry.contactRingtoneUri = oldEntry.contactRingtoneUri;
+ }
+
+ // If no image and it's a business, switch to using the default business avatar.
+ if (info.getImageUrl() == null && info.isBusiness()) {
+ Log.d(TAG, "Business has no image. Using default.");
+ entry.photo = mContext.getResources().getDrawable(R.drawable.img_business);
+ entry.photoType = ContactPhotoType.BUSINESS;
+ }
+
+ mInfoMap.put(mCallId, entry);
+ }
+ sendInfoNotifications(mCallId, entry);
+
+ entry.hasPhotoToLoad = info.getImageUrl() != null;
+
+ // If there is no image then we should not expect another callback.
+ if (!entry.hasPhotoToLoad) {
+ // We're done, so clear callbacks
+ clearCallbacks(mCallId);
+ }
+ }
+
+ @Override
+ public void onImageFetchComplete(Bitmap bitmap) {
+ loadImage(null, bitmap, mCallId);
+ onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, bitmap, mCallId);
+ }
+ }
+}
diff --git a/java/com/android/incallui/ContactsAsyncHelper.java b/java/com/android/incallui/ContactsAsyncHelper.java
new file mode 100644
index 000000000..08ff74d0e
--- /dev/null
+++ b/java/com/android/incallui/ContactsAsyncHelper.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui;
+
+import android.app.Notification;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.support.annotation.MainThread;
+import android.support.annotation.WorkerThread;
+import java.io.IOException;
+import java.io.InputStream;
+
+/** Helper class for loading contacts photo asynchronously. */
+public class ContactsAsyncHelper {
+
+ /** Interface for a WorkerHandler result return. */
+ public interface OnImageLoadCompleteListener {
+
+ /**
+ * Called when the image load is complete. Must be called in main thread.
+ *
+ * @param token Integer passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int, Context,
+ * Uri, OnImageLoadCompleteListener, Object)}.
+ * @param photo Drawable object obtained by the async load.
+ * @param photoIcon Bitmap object obtained by the async load.
+ * @param cookie Object passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int, Context,
+ * Uri, OnImageLoadCompleteListener, Object)}. Can be null iff. the original cookie is null.
+ */
+ @MainThread
+ void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie);
+
+ /** Called when image is loaded to udpate data. Must be called in worker thread. */
+ @WorkerThread
+ void onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie);
+ }
+
+ // constants
+ private static final int EVENT_LOAD_IMAGE = 1;
+ /** Handler run on a worker thread to load photo asynchronously. */
+ private static Handler sThreadHandler;
+ /** For forcing the system to call its constructor */
+ @SuppressWarnings("unused")
+ private static ContactsAsyncHelper sInstance;
+
+ static {
+ sInstance = new ContactsAsyncHelper();
+ }
+
+ private final Handler mResultHandler =
+ /** A handler that handles message to call listener notifying UI change on main thread. */
+ new Handler(Looper.getMainLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ WorkerArgs args = (WorkerArgs) msg.obj;
+ switch (msg.arg1) {
+ case EVENT_LOAD_IMAGE:
+ if (args.listener != null) {
+ Log.d(
+ this,
+ "Notifying listener: "
+ + args.listener.toString()
+ + " image: "
+ + args.displayPhotoUri
+ + " completed");
+ args.listener.onImageLoadComplete(
+ msg.what, args.photo, args.photoIcon, args.cookie);
+ }
+ break;
+ default:
+ }
+ }
+ };
+
+ /** Private constructor for static class */
+ private ContactsAsyncHelper() {
+ HandlerThread thread = new HandlerThread("ContactsAsyncWorker");
+ thread.start();
+ sThreadHandler = new WorkerHandler(thread.getLooper());
+ }
+
+ /**
+ * Starts an asynchronous image load. After finishing the load, {@link
+ * OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)} will be called.
+ *
+ * @param token Arbitrary integer which will be returned as the first argument of {@link
+ * OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
+ * @param context Context object used to do the time-consuming operation.
+ * @param displayPhotoUri Uri to be used to fetch the photo
+ * @param listener Callback object which will be used when the asynchronous load is done. Can be
+ * null, which means only the asynchronous load is done while there's no way to obtain the
+ * loaded photos.
+ * @param cookie Arbitrary object the caller wants to remember, which will become the fourth
+ * argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap,
+ * Object)}. Can be null, at which the callback will also has null for the argument.
+ */
+ public static final void startObtainPhotoAsync(
+ int token,
+ Context context,
+ Uri displayPhotoUri,
+ OnImageLoadCompleteListener listener,
+ Object cookie) {
+ // in case the source caller info is null, the URI will be null as well.
+ // just update using the placeholder image in this case.
+ if (displayPhotoUri == null) {
+ Log.e("startObjectPhotoAsync", "Uri is missing");
+ return;
+ }
+
+ // Added additional Cookie field in the callee to handle arguments
+ // sent to the callback function.
+
+ // setup arguments
+ WorkerArgs args = new WorkerArgs();
+ args.cookie = cookie;
+ args.context = context;
+ args.displayPhotoUri = displayPhotoUri;
+ args.listener = listener;
+
+ // setup message arguments
+ Message msg = sThreadHandler.obtainMessage(token);
+ msg.arg1 = EVENT_LOAD_IMAGE;
+ msg.obj = args;
+
+ Log.d(
+ "startObjectPhotoAsync",
+ "Begin loading image: " + args.displayPhotoUri + ", displaying default image for now.");
+
+ // notify the thread to begin working
+ sThreadHandler.sendMessage(msg);
+ }
+
+ private static final class WorkerArgs {
+
+ public Context context;
+ public Uri displayPhotoUri;
+ public Drawable photo;
+ public Bitmap photoIcon;
+ public Object cookie;
+ public OnImageLoadCompleteListener listener;
+ }
+
+ /** Thread worker class that handles the task of opening the stream and loading the images. */
+ private class WorkerHandler extends Handler {
+
+ public WorkerHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ WorkerArgs args = (WorkerArgs) msg.obj;
+
+ switch (msg.arg1) {
+ case EVENT_LOAD_IMAGE:
+ InputStream inputStream = null;
+ try {
+ try {
+ inputStream = args.context.getContentResolver().openInputStream(args.displayPhotoUri);
+ } catch (Exception e) {
+ Log.e(this, "Error opening photo input stream", e);
+ }
+
+ if (inputStream != null) {
+ args.photo = Drawable.createFromStream(inputStream, args.displayPhotoUri.toString());
+
+ // This assumes Drawable coming from contact database is usually
+ // BitmapDrawable and thus we can have (down)scaled version of it.
+ args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo);
+
+ Log.d(
+ ContactsAsyncHelper.this,
+ "Loading image: "
+ + msg.arg1
+ + " token: "
+ + msg.what
+ + " image URI: "
+ + args.displayPhotoUri);
+ } else {
+ args.photo = null;
+ args.photoIcon = null;
+ Log.d(
+ ContactsAsyncHelper.this,
+ "Problem with image: "
+ + msg.arg1
+ + " token: "
+ + msg.what
+ + " image URI: "
+ + args.displayPhotoUri
+ + ", using default image.");
+ }
+ if (args.listener != null) {
+ args.listener.onImageLoaded(msg.what, args.photo, args.photoIcon, args.cookie);
+ }
+ } finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ Log.e(this, "Unable to close input stream.", e);
+ }
+ }
+ }
+ break;
+ default:
+ }
+
+ // send the reply to the enclosing class.
+ Message reply = ContactsAsyncHelper.this.mResultHandler.obtainMessage(msg.what);
+ reply.arg1 = msg.arg1;
+ reply.obj = msg.obj;
+ reply.sendToTarget();
+ }
+
+ /**
+ * Returns a Bitmap object suitable for {@link Notification}'s large icon. This might return
+ * null when the given Drawable isn't BitmapDrawable, or if the system fails to create a scaled
+ * Bitmap for the Drawable.
+ */
+ private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) {
+ if (!(photo instanceof BitmapDrawable)) {
+ return null;
+ }
+ int iconSize = context.getResources().getDimensionPixelSize(R.dimen.notification_icon_size);
+ Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap();
+ int orgWidth = orgBitmap.getWidth();
+ int orgHeight = orgBitmap.getHeight();
+ int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight;
+ // We want downscaled one only when the original icon is too big.
+ if (longerEdge > iconSize) {
+ float ratio = ((float) longerEdge) / iconSize;
+ int newWidth = (int) (orgWidth / ratio);
+ int newHeight = (int) (orgHeight / ratio);
+ // If the longer edge is much longer than the shorter edge, the latter may
+ // become 0 which will cause a crash.
+ if (newWidth <= 0 || newHeight <= 0) {
+ Log.w(this, "Photo icon's width or height become 0.");
+ return null;
+ }
+
+ // It is sure ratio >= 1.0f in any case and thus the newly created Bitmap
+ // should be smaller than the original.
+ return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true);
+ } else {
+ return orgBitmap;
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/ContactsPreferencesFactory.java b/java/com/android/incallui/ContactsPreferencesFactory.java
new file mode 100644
index 000000000..429de7bc9
--- /dev/null
+++ b/java/com/android/incallui/ContactsPreferencesFactory.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.support.v4.os.UserManagerCompat;
+import com.android.contacts.common.preference.ContactsPreferences;
+
+/** Factory class for {@link ContactsPreferences}. */
+public class ContactsPreferencesFactory {
+
+ private static boolean sUseTestInstance;
+ private static ContactsPreferences sTestInstance;
+
+ /**
+ * Creates a new {@link ContactsPreferences} object if possible.
+ *
+ * @param context the context to use when creating the ContactsPreferences.
+ * @return a new ContactsPreferences object or {@code null} if the user is locked.
+ */
+ @Nullable
+ public static ContactsPreferences newContactsPreferences(Context context) {
+ if (sUseTestInstance) {
+ return sTestInstance;
+ }
+ if (UserManagerCompat.isUserUnlocked(context)) {
+ return new ContactsPreferences(context);
+ }
+ return null;
+ }
+
+ /**
+ * Sets the instance to be returned by all calls to {@link #newContactsPreferences(Context)}.
+ *
+ * @param testInstance the instance to return.
+ */
+ static void setTestInstance(ContactsPreferences testInstance) {
+ sUseTestInstance = true;
+ sTestInstance = testInstance;
+ }
+}
diff --git a/java/com/android/incallui/DialpadFragment.java b/java/com/android/incallui/DialpadFragment.java
new file mode 100644
index 000000000..7f494aa61
--- /dev/null
+++ b/java/com/android/incallui/DialpadFragment.java
@@ -0,0 +1,461 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.method.DialerKeyListener;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnKeyListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.dialer.dialpadview.DialpadKeyButton;
+import com.android.dialer.dialpadview.DialpadKeyButton.OnPressedListener;
+import com.android.dialer.dialpadview.DialpadView;
+import com.android.incallui.DialpadPresenter.DialpadUi;
+import com.android.incallui.baseui.BaseFragment;
+import java.util.Map;
+
+/** Fragment for call control buttons */
+public class DialpadFragment extends BaseFragment<DialpadPresenter, DialpadUi>
+ implements DialpadUi, OnKeyListener, OnClickListener, OnPressedListener {
+
+ /** Hash Map to map a view id to a character */
+ private static final Map<Integer, Character> mDisplayMap = new ArrayMap<>();
+
+ /** Set up the static maps */
+ static {
+ // Map the buttons to the display characters
+ mDisplayMap.put(R.id.one, '1');
+ mDisplayMap.put(R.id.two, '2');
+ mDisplayMap.put(R.id.three, '3');
+ mDisplayMap.put(R.id.four, '4');
+ mDisplayMap.put(R.id.five, '5');
+ mDisplayMap.put(R.id.six, '6');
+ mDisplayMap.put(R.id.seven, '7');
+ mDisplayMap.put(R.id.eight, '8');
+ mDisplayMap.put(R.id.nine, '9');
+ mDisplayMap.put(R.id.zero, '0');
+ mDisplayMap.put(R.id.pound, '#');
+ mDisplayMap.put(R.id.star, '*');
+ }
+
+ private final int[] mButtonIds =
+ new int[] {
+ R.id.zero,
+ R.id.one,
+ R.id.two,
+ R.id.three,
+ R.id.four,
+ R.id.five,
+ R.id.six,
+ R.id.seven,
+ R.id.eight,
+ R.id.nine,
+ R.id.star,
+ R.id.pound
+ };
+ private EditText mDtmfDialerField;
+ // KeyListener used with the "dialpad digits" EditText widget.
+ private DTMFKeyListener mDialerKeyListener;
+ private DialpadView mDialpadView;
+ private int mCurrentTextColor;
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.dialpad_back) {
+ getActivity().onBackPressed();
+ }
+ }
+
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ Log.d(this, "onKey: keyCode " + keyCode + ", view " + v);
+
+ if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) {
+ int viewId = v.getId();
+ if (mDisplayMap.containsKey(viewId)) {
+ switch (event.getAction()) {
+ case KeyEvent.ACTION_DOWN:
+ if (event.getRepeatCount() == 0) {
+ getPresenter().processDtmf(mDisplayMap.get(viewId));
+ }
+ break;
+ case KeyEvent.ACTION_UP:
+ getPresenter().stopDtmf();
+ break;
+ }
+ // do not return true [handled] here, since we want the
+ // press / click animation to be handled by the framework.
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public DialpadPresenter createPresenter() {
+ return new DialpadPresenter();
+ }
+
+ @Override
+ public DialpadPresenter.DialpadUi getUi() {
+ return this;
+ }
+
+ // TODO Adds hardware keyboard listener
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View parent = inflater.inflate(R.layout.incall_dialpad_fragment, container, false);
+ mDialpadView = (DialpadView) parent.findViewById(R.id.dialpad_view);
+ mDialpadView.setCanDigitsBeEdited(false);
+ mDialpadView.setBackgroundResource(R.color.incall_dialpad_background);
+ mDtmfDialerField = (EditText) parent.findViewById(R.id.digits);
+ if (mDtmfDialerField != null) {
+ mDialerKeyListener = new DTMFKeyListener();
+ mDtmfDialerField.setKeyListener(mDialerKeyListener);
+ // remove the long-press context menus that support
+ // the edit (copy / paste / select) functions.
+ mDtmfDialerField.setLongClickable(false);
+ mDtmfDialerField.setElegantTextHeight(false);
+ configureKeypadListeners();
+ }
+ View backButton = mDialpadView.findViewById(R.id.dialpad_back);
+ backButton.setVisibility(View.VISIBLE);
+ backButton.setOnClickListener(this);
+
+ return parent;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ updateColors();
+ }
+
+ public void updateColors() {
+ int textColor = InCallPresenter.getInstance().getThemeColorManager().getPrimaryColor();
+
+ if (mCurrentTextColor == textColor) {
+ return;
+ }
+
+ DialpadKeyButton dialpadKey;
+ for (int i = 0; i < mButtonIds.length; i++) {
+ dialpadKey = (DialpadKeyButton) mDialpadView.findViewById(mButtonIds[i]);
+ ((TextView) dialpadKey.findViewById(R.id.dialpad_key_number)).setTextColor(textColor);
+ }
+
+ mCurrentTextColor = textColor;
+ }
+
+ @Override
+ public void onDestroyView() {
+ mDialerKeyListener = null;
+ super.onDestroyView();
+ }
+
+ /**
+ * Getter for Dialpad text.
+ *
+ * @return String containing current Dialpad EditText text.
+ */
+ public String getDtmfText() {
+ return mDtmfDialerField.getText().toString();
+ }
+
+ /**
+ * Sets the Dialpad text field with some text.
+ *
+ * @param text Text to set Dialpad EditText to.
+ */
+ public void setDtmfText(String text) {
+ mDtmfDialerField.setText(PhoneNumberUtilsCompat.createTtsSpannable(text));
+ }
+
+ @Override
+ public void setVisible(boolean on) {
+ if (on) {
+ getView().setVisibility(View.VISIBLE);
+ } else {
+ getView().setVisibility(View.INVISIBLE);
+ }
+ }
+
+ /** Starts the slide up animation for the Dialpad keys when the Dialpad is revealed. */
+ public void animateShowDialpad() {
+ final DialpadView dialpadView = (DialpadView) getView().findViewById(R.id.dialpad_view);
+ dialpadView.animateShow();
+ }
+
+ @Override
+ public void appendDigitsToField(char digit) {
+ if (mDtmfDialerField != null) {
+ // TODO: maybe *don't* manually append this digit if
+ // mDialpadDigits is focused and this key came from the HW
+ // keyboard, since in that case the EditText field will
+ // get the key event directly and automatically appends
+ // whetever the user types.
+ // (Or, a cleaner fix would be to just make mDialpadDigits
+ // *not* handle HW key presses. That seems to be more
+ // complicated than just setting focusable="false" on it,
+ // though.)
+ mDtmfDialerField.getText().append(digit);
+ }
+ }
+
+ /** Called externally (from InCallScreen) to play a DTMF Tone. */
+ /* package */ boolean onDialerKeyDown(KeyEvent event) {
+ Log.d(this, "Notifying dtmf key down.");
+ if (mDialerKeyListener != null) {
+ return mDialerKeyListener.onKeyDown(event);
+ } else {
+ return false;
+ }
+ }
+
+ /** Called externally (from InCallScreen) to cancel the last DTMF Tone played. */
+ public boolean onDialerKeyUp(KeyEvent event) {
+ Log.d(this, "Notifying dtmf key up.");
+ if (mDialerKeyListener != null) {
+ return mDialerKeyListener.onKeyUp(event);
+ } else {
+ return false;
+ }
+ }
+
+ private void configureKeypadListeners() {
+ DialpadKeyButton dialpadKey;
+ for (int i = 0; i < mButtonIds.length; i++) {
+ dialpadKey = (DialpadKeyButton) mDialpadView.findViewById(mButtonIds[i]);
+ dialpadKey.setOnKeyListener(this);
+ dialpadKey.setOnClickListener(this);
+ dialpadKey.setOnPressedListener(this);
+ }
+ }
+
+ @Override
+ public void onPressed(View view, boolean pressed) {
+ if (pressed && mDisplayMap.containsKey(view.getId())) {
+ Log.d(this, "onPressed: " + pressed + " " + mDisplayMap.get(view.getId()));
+ getPresenter().processDtmf(mDisplayMap.get(view.getId()));
+ }
+ if (!pressed) {
+ Log.d(this, "onPressed: " + pressed);
+ getPresenter().stopDtmf();
+ }
+ }
+
+ /**
+ * LinearLayout with getter and setter methods for the translationY property using floats, for
+ * animation purposes.
+ */
+ public static class DialpadSlidingLinearLayout extends LinearLayout {
+
+ public DialpadSlidingLinearLayout(Context context) {
+ super(context);
+ }
+
+ public DialpadSlidingLinearLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public DialpadSlidingLinearLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public float getYFraction() {
+ final int height = getHeight();
+ if (height == 0) {
+ return 0;
+ }
+ return getTranslationY() / height;
+ }
+
+ public void setYFraction(float yFraction) {
+ setTranslationY(yFraction * getHeight());
+ }
+ }
+
+ /**
+ * Our own key listener, specialized for dealing with DTMF codes. 1. Ignore the backspace since it
+ * is irrelevant. 2. Allow ONLY valid DTMF characters to generate a tone and be sent as a DTMF
+ * code. 3. All other remaining characters are handled by the superclass.
+ *
+ * <p>This code is purely here to handle events from the hardware keyboard while the DTMF dialpad
+ * is up.
+ */
+ private class DTMFKeyListener extends DialerKeyListener {
+
+ /**
+ * Overrides the characters used in {@link DialerKeyListener#CHARACTERS} These are the valid
+ * dtmf characters.
+ */
+ public final char[] DTMF_CHARACTERS =
+ new char[] {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '*'};
+
+ private DTMFKeyListener() {
+ super();
+ }
+
+ /** Overriden to return correct DTMF-dialable characters. */
+ @Override
+ protected char[] getAcceptedChars() {
+ return DTMF_CHARACTERS;
+ }
+
+ /** special key listener ignores backspace. */
+ @Override
+ public boolean backspace(View view, Editable content, int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Overriden so that with each valid button press, we start sending a dtmf code and play a local
+ * dtmf tone.
+ */
+ @Override
+ public boolean onKeyDown(View view, Editable content, int keyCode, KeyEvent event) {
+ // if (DBG) log("DTMFKeyListener.onKeyDown, keyCode " + keyCode + ", view " + view);
+
+ // find the character
+ char c = (char) lookup(event, content);
+
+ // if not a long press, and parent onKeyDown accepts the input
+ if (event.getRepeatCount() == 0 && super.onKeyDown(view, content, keyCode, event)) {
+
+ boolean keyOK = ok(getAcceptedChars(), c);
+
+ // if the character is a valid dtmf code, start playing the tone and send the
+ // code.
+ if (keyOK) {
+ Log.d(this, "DTMFKeyListener reading '" + c + "' from input.");
+ getPresenter().processDtmf(c);
+ } else {
+ Log.d(this, "DTMFKeyListener rejecting '" + c + "' from input.");
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Overriden so that with each valid button up, we stop sending a dtmf code and the dtmf tone.
+ */
+ @Override
+ public boolean onKeyUp(View view, Editable content, int keyCode, KeyEvent event) {
+ // if (DBG) log("DTMFKeyListener.onKeyUp, keyCode " + keyCode + ", view " + view);
+
+ super.onKeyUp(view, content, keyCode, event);
+
+ // find the character
+ char c = (char) lookup(event, content);
+
+ boolean keyOK = ok(getAcceptedChars(), c);
+
+ if (keyOK) {
+ Log.d(this, "Stopping the tone for '" + c + "'");
+ getPresenter().stopDtmf();
+ return true;
+ }
+
+ return false;
+ }
+
+ /** Handle individual keydown events when we DO NOT have an Editable handy. */
+ public boolean onKeyDown(KeyEvent event) {
+ char c = lookup(event);
+ Log.d(this, "DTMFKeyListener.onKeyDown: event '" + c + "'");
+
+ // if not a long press, and parent onKeyDown accepts the input
+ if (event.getRepeatCount() == 0 && c != 0) {
+ // if the character is a valid dtmf code, start playing the tone and send the
+ // code.
+ if (ok(getAcceptedChars(), c)) {
+ Log.d(this, "DTMFKeyListener reading '" + c + "' from input.");
+ getPresenter().processDtmf(c);
+ return true;
+ } else {
+ Log.d(this, "DTMFKeyListener rejecting '" + c + "' from input.");
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Handle individual keyup events.
+ *
+ * @param event is the event we are trying to stop. If this is null, then we just force-stop the
+ * last tone without checking if the event is an acceptable dialer event.
+ */
+ public boolean onKeyUp(KeyEvent event) {
+ if (event == null) {
+ //the below piece of code sends stopDTMF event unnecessarily even when a null event
+ //is received, hence commenting it.
+ /*if (DBG) log("Stopping the last played tone.");
+ stopTone();*/
+ return true;
+ }
+
+ char c = lookup(event);
+ Log.d(this, "DTMFKeyListener.onKeyUp: event '" + c + "'");
+
+ // TODO: stopTone does not take in character input, we may want to
+ // consider checking for this ourselves.
+ if (ok(getAcceptedChars(), c)) {
+ Log.d(this, "Stopping the tone for '" + c + "'");
+ getPresenter().stopDtmf();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Find the Dialer Key mapped to this event.
+ *
+ * @return The char value of the input event, otherwise 0 if no matching character was found.
+ */
+ private char lookup(KeyEvent event) {
+ // This code is similar to {@link DialerKeyListener#lookup(KeyEvent, Spannable) lookup}
+ int meta = event.getMetaState();
+ int number = event.getNumber();
+
+ if (!((meta & (KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON)) == 0) || (number == 0)) {
+ int match = event.getMatch(getAcceptedChars(), meta);
+ number = (match != 0) ? match : number;
+ }
+
+ return (char) number;
+ }
+
+ /** Check to see if the keyEvent is dialable. */
+ boolean isKeyEventAcceptable(KeyEvent event) {
+ return (ok(getAcceptedChars(), lookup(event)));
+ }
+ }
+}
diff --git a/java/com/android/incallui/DialpadPresenter.java b/java/com/android/incallui/DialpadPresenter.java
new file mode 100644
index 000000000..7a784c279
--- /dev/null
+++ b/java/com/android/incallui/DialpadPresenter.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui;
+
+import android.telephony.PhoneNumberUtils;
+import com.android.incallui.DialpadPresenter.DialpadUi;
+import com.android.incallui.baseui.Presenter;
+import com.android.incallui.baseui.Ui;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.TelecomAdapter;
+
+/** Logic for call buttons. */
+public class DialpadPresenter extends Presenter<DialpadUi>
+ implements InCallPresenter.InCallStateListener {
+
+ private DialerCall mCall;
+
+ @Override
+ public void onUiReady(DialpadUi ui) {
+ super.onUiReady(ui);
+ InCallPresenter.getInstance().addListener(this);
+ mCall = CallList.getInstance().getOutgoingOrActive();
+ }
+
+ @Override
+ public void onUiUnready(DialpadUi ui) {
+ super.onUiUnready(ui);
+ InCallPresenter.getInstance().removeListener(this);
+ }
+
+ @Override
+ public void onStateChange(
+ InCallPresenter.InCallState oldState,
+ InCallPresenter.InCallState newState,
+ CallList callList) {
+ mCall = callList.getOutgoingOrActive();
+ Log.d(this, "DialpadPresenter mCall = " + mCall);
+ }
+
+ /**
+ * Processes the specified digit as a DTMF key, by playing the appropriate DTMF tone, and
+ * appending the digit to the EditText field that displays the DTMF digits sent so far.
+ */
+ public final void processDtmf(char c) {
+ Log.d(this, "Processing dtmf key " + c);
+ // if it is a valid key, then update the display and send the dtmf tone.
+ if (PhoneNumberUtils.is12Key(c) && mCall != null) {
+ Log.d(this, "updating display and sending dtmf tone for '" + c + "'");
+
+ // Append this key to the "digits" widget.
+ DialpadUi dialpadUi = getUi();
+ if (dialpadUi != null) {
+ dialpadUi.appendDigitsToField(c);
+ }
+ // Plays the tone through Telecom.
+ TelecomAdapter.getInstance().playDtmfTone(mCall.getId(), c);
+ } else {
+ Log.d(this, "ignoring dtmf request for '" + c + "'");
+ }
+ }
+
+ /** Stops the local tone based on the phone type. */
+ public void stopDtmf() {
+ if (mCall != null) {
+ Log.d(this, "stopping remote tone");
+ TelecomAdapter.getInstance().stopDtmfTone(mCall.getId());
+ }
+ }
+
+ public interface DialpadUi extends Ui {
+
+ void setVisible(boolean on);
+
+ void appendDigitsToField(char digit);
+ }
+}
diff --git a/java/com/android/incallui/ExternalCallNotifier.java b/java/com/android/incallui/ExternalCallNotifier.java
new file mode 100644
index 000000000..466e12a6d
--- /dev/null
+++ b/java/com/android/incallui/ExternalCallNotifier.java
@@ -0,0 +1,465 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui;
+
+import android.annotation.TargetApi;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telecom.Call;
+import android.telecom.PhoneAccount;
+import android.telecom.VideoProfile;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.compat.CallCompat;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.common.util.BitmapUtil;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCallDelegate;
+import com.android.incallui.call.ExternalCallList;
+import com.android.incallui.latencyreport.LatencyReport;
+import com.android.incallui.util.TelecomCallUtil;
+import java.util.Map;
+
+/**
+ * Handles the display of notifications for "external calls".
+ *
+ * <p>External calls are a representation of a call which is in progress on the user's other device
+ * (e.g. another phone, or a watch).
+ */
+public class ExternalCallNotifier implements ExternalCallList.ExternalCallListener {
+
+ /** Tag used with the notification manager to uniquely identify external call notifications. */
+ private static final String NOTIFICATION_TAG = "EXTERNAL_CALL";
+
+ private static final int SUMMARY_ID = -1;
+ private final Context mContext;
+ private final ContactInfoCache mContactInfoCache;
+ private Map<Call, NotificationInfo> mNotifications = new ArrayMap<>();
+ private int mNextUniqueNotificationId;
+ private ContactsPreferences mContactsPreferences;
+ private boolean mShowingSummary;
+
+ /** Initializes a new instance of the external call notifier. */
+ public ExternalCallNotifier(
+ @NonNull Context context, @NonNull ContactInfoCache contactInfoCache) {
+ mContext = context;
+ mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext);
+ mContactInfoCache = contactInfoCache;
+ }
+
+ /**
+ * Handles the addition of a new external call by showing a new notification. Triggered by {@link
+ * CallList#onCallAdded(android.telecom.Call)}.
+ */
+ @Override
+ public void onExternalCallAdded(android.telecom.Call call) {
+ Log.i(this, "onExternalCallAdded " + call);
+ if (mNotifications.containsKey(call)) {
+ throw new IllegalArgumentException();
+ }
+ NotificationInfo info = new NotificationInfo(call, mNextUniqueNotificationId++);
+ mNotifications.put(call, info);
+
+ showNotifcation(info);
+ }
+
+ /**
+ * Handles the removal of an external call by hiding its associated notification. Triggered by
+ * {@link CallList#onCallRemoved(android.telecom.Call)}.
+ */
+ @Override
+ public void onExternalCallRemoved(android.telecom.Call call) {
+ Log.i(this, "onExternalCallRemoved " + call);
+
+ dismissNotification(call);
+ }
+
+ /** Handles updates to an external call. */
+ @Override
+ public void onExternalCallUpdated(Call call) {
+ if (!mNotifications.containsKey(call)) {
+ throw new IllegalArgumentException();
+ }
+ postNotification(mNotifications.get(call));
+ }
+
+ @Override
+ public void onExternalCallPulled(Call call) {
+ // no-op; if an external call is pulled, it will be removed via onExternalCallRemoved.
+ }
+
+ /**
+ * Initiates a call pull given a notification ID.
+ *
+ * @param notificationId The notification ID associated with the external call which is to be
+ * pulled.
+ */
+ @TargetApi(VERSION_CODES.N_MR1)
+ public void pullExternalCall(int notificationId) {
+ for (NotificationInfo info : mNotifications.values()) {
+ if (info.getNotificationId() == notificationId
+ && CallCompat.canPullExternalCall(info.getCall())) {
+ info.getCall().pullExternalCall();
+ return;
+ }
+ }
+ }
+
+ /**
+ * Shows a notification for a new external call. Performs a contact cache lookup to find any
+ * associated photo and information for the call.
+ */
+ private void showNotifcation(final NotificationInfo info) {
+ // We make a call to the contact info cache to query for supplemental data to what the
+ // call provides. This includes the contact name and photo.
+ // This callback will always get called immediately and synchronously with whatever data
+ // it has available, and may make a subsequent call later (same thread) if it had to
+ // call into the contacts provider for more data.
+ DialerCall dialerCall =
+ new DialerCall(
+ mContext,
+ new DialerCallDelegateStub(),
+ info.getCall(),
+ new LatencyReport(),
+ false /* registerCallback */);
+
+ mContactInfoCache.findInfo(
+ dialerCall,
+ false /* isIncoming */,
+ new ContactInfoCache.ContactInfoCacheCallback() {
+ @Override
+ public void onContactInfoComplete(
+ String callId, ContactInfoCache.ContactCacheEntry entry) {
+
+ // Ensure notification still exists as the external call could have been
+ // removed during async contact info lookup.
+ if (mNotifications.containsKey(info.getCall())) {
+ saveContactInfo(info, entry);
+ }
+ }
+
+ @Override
+ public void onImageLoadComplete(String callId, ContactInfoCache.ContactCacheEntry entry) {
+
+ // Ensure notification still exists as the external call could have been
+ // removed during async contact info lookup.
+ if (mNotifications.containsKey(info.getCall())) {
+ savePhoto(info, entry);
+ }
+ }
+ });
+ }
+
+ /** Dismisses a notification for an external call. */
+ private void dismissNotification(Call call) {
+ if (!mNotifications.containsKey(call)) {
+ throw new IllegalArgumentException();
+ }
+
+ NotificationManager notificationManager =
+ (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.cancel(NOTIFICATION_TAG, mNotifications.get(call).getNotificationId());
+
+ mNotifications.remove(call);
+
+ if (mShowingSummary && mNotifications.size() <= 1) {
+ // Where a summary notification is showing and there is now not enough notifications to
+ // necessitate a summary, cancel the summary.
+ notificationManager.cancel(NOTIFICATION_TAG, SUMMARY_ID);
+ mShowingSummary = false;
+
+ // If there is still a single call requiring a notification, re-post the notification as a
+ // standalone notification without a summary notification.
+ if (mNotifications.size() == 1) {
+ postNotification(mNotifications.values().iterator().next());
+ }
+ }
+ }
+
+ /**
+ * Attempts to build a large icon to use for the notification based on the contact info and post
+ * the updated notification to the notification manager.
+ */
+ private void savePhoto(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry) {
+ Bitmap largeIcon = getLargeIconToDisplay(mContext, entry, info.getCall());
+ if (largeIcon != null) {
+ largeIcon = getRoundedIcon(mContext, largeIcon);
+ }
+ info.setLargeIcon(largeIcon);
+ postNotification(info);
+ }
+
+ /**
+ * Builds and stores the contact information the notification will display and posts the updated
+ * notification to the notification manager.
+ */
+ private void saveContactInfo(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry) {
+ info.setContentTitle(getContentTitle(mContext, mContactsPreferences, entry, info.getCall()));
+ info.setPersonReference(getPersonReference(entry, info.getCall()));
+ postNotification(info);
+ }
+
+ /** Rebuild an existing or show a new notification given {@link NotificationInfo}. */
+ private void postNotification(NotificationInfo info) {
+ Notification.Builder builder = new Notification.Builder(mContext);
+ // Set notification as ongoing since calls are long-running versus a point-in-time notice.
+ builder.setOngoing(true);
+ // Make the notification prioritized over the other normal notifications.
+ builder.setPriority(Notification.PRIORITY_HIGH);
+ builder.setGroup(NOTIFICATION_TAG);
+
+ boolean isVideoCall = VideoProfile.isVideo(info.getCall().getDetails().getVideoState());
+ // Set the content ("Ongoing call on another device")
+ builder.setContentText(
+ mContext.getString(
+ isVideoCall
+ ? R.string.notification_external_video_call
+ : R.string.notification_external_call));
+ builder.setSmallIcon(R.drawable.quantum_ic_call_white_24);
+ builder.setContentTitle(info.getContentTitle());
+ builder.setLargeIcon(info.getLargeIcon());
+ builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
+ builder.addPerson(info.getPersonReference());
+
+ // Where the external call supports being transferred to the local device, add an action
+ // to the notification to initiate the call pull process.
+ if (CallCompat.canPullExternalCall(info.getCall())) {
+
+ Intent intent =
+ new Intent(
+ NotificationBroadcastReceiver.ACTION_PULL_EXTERNAL_CALL,
+ null,
+ mContext,
+ NotificationBroadcastReceiver.class);
+ intent.putExtra(
+ NotificationBroadcastReceiver.EXTRA_NOTIFICATION_ID, info.getNotificationId());
+ builder.addAction(
+ new Notification.Action.Builder(
+ R.drawable.quantum_ic_call_white_24,
+ mContext.getString(
+ isVideoCall
+ ? R.string.notification_take_video_call
+ : R.string.notification_take_call),
+ PendingIntent.getBroadcast(mContext, info.getNotificationId(), intent, 0))
+ .build());
+ }
+
+ /**
+ * This builder is used for the notification shown when the device is locked and the user has
+ * set their notification settings to 'hide sensitive content' {@see
+ * Notification.Builder#setPublicVersion}.
+ */
+ Notification.Builder publicBuilder = new Notification.Builder(mContext);
+ publicBuilder.setSmallIcon(R.drawable.quantum_ic_call_white_24);
+ publicBuilder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
+
+ builder.setPublicVersion(publicBuilder.build());
+ Notification notification = builder.build();
+
+ NotificationManager notificationManager =
+ (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.notify(NOTIFICATION_TAG, info.getNotificationId(), notification);
+
+ if (!mShowingSummary && mNotifications.size() > 1) {
+ // If the number of notifications shown is > 1, and we're not already showing a group summary,
+ // build one now. This will ensure the like notifications are grouped together.
+
+ Notification.Builder summary = new Notification.Builder(mContext);
+ // Set notification as ongoing since calls are long-running versus a point-in-time notice.
+ summary.setOngoing(true);
+ // Make the notification prioritized over the other normal notifications.
+ summary.setPriority(Notification.PRIORITY_HIGH);
+ summary.setGroup(NOTIFICATION_TAG);
+ summary.setGroupSummary(true);
+ summary.setSmallIcon(R.drawable.quantum_ic_call_white_24);
+ notificationManager.notify(NOTIFICATION_TAG, SUMMARY_ID, summary.build());
+ mShowingSummary = true;
+ }
+ }
+
+ /**
+ * Finds a large icon to display in a notification for a call. For conference calls, a conference
+ * call icon is used, otherwise if contact info is specified, the user's contact photo or avatar
+ * is used.
+ *
+ * @param context The context.
+ * @param contactInfo The contact cache info.
+ * @param call The call.
+ * @return The large icon to use for the notification.
+ */
+ private @Nullable Bitmap getLargeIconToDisplay(
+ Context context, ContactInfoCache.ContactCacheEntry contactInfo, android.telecom.Call call) {
+
+ Bitmap largeIcon = null;
+ if (call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE)
+ && !call.getDetails()
+ .hasProperty(android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE)) {
+
+ largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.img_conference);
+ }
+ if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) {
+ largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap();
+ }
+ return largeIcon;
+ }
+
+ /**
+ * Given a bitmap, returns a rounded version of the icon suitable for display in a notification.
+ *
+ * @param context The context.
+ * @param bitmap The bitmap to round.
+ * @return The rounded bitmap.
+ */
+ private @Nullable Bitmap getRoundedIcon(Context context, @Nullable Bitmap bitmap) {
+ if (bitmap == null) {
+ return null;
+ }
+ final int height =
+ (int) context.getResources().getDimension(android.R.dimen.notification_large_icon_height);
+ final int width =
+ (int) context.getResources().getDimension(android.R.dimen.notification_large_icon_width);
+ return BitmapUtil.getRoundedBitmap(bitmap, width, height);
+ }
+
+ /**
+ * Builds a notification content title for a call. If the call is a conference call, it is
+ * identified as such. Otherwise an attempt is made to show an associated contact name or phone
+ * number.
+ *
+ * @param context The context.
+ * @param contactsPreferences Contacts preferences, used to determine the preferred formatting for
+ * contact names.
+ * @param contactInfo The contact info which was looked up in the contact cache.
+ * @param call The call to generate a title for.
+ * @return The content title.
+ */
+ private @Nullable String getContentTitle(
+ Context context,
+ @Nullable ContactsPreferences contactsPreferences,
+ ContactInfoCache.ContactCacheEntry contactInfo,
+ android.telecom.Call call) {
+
+ if (call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE)
+ && !call.getDetails()
+ .hasProperty(android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE)) {
+
+ return context.getResources().getString(R.string.conference_call_name);
+ }
+
+ String preferredName =
+ ContactDisplayUtils.getPreferredDisplayName(
+ contactInfo.namePrimary, contactInfo.nameAlternative, contactsPreferences);
+ if (TextUtils.isEmpty(preferredName)) {
+ return TextUtils.isEmpty(contactInfo.number)
+ ? null
+ : BidiFormatter.getInstance()
+ .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR);
+ }
+ return preferredName;
+ }
+
+ /**
+ * Gets a "person reference" for a notification, used by the system to determine whether the
+ * notification should be allowed past notification interruption filters.
+ *
+ * @param contactInfo The contact info from cache.
+ * @param call The call.
+ * @return the person reference.
+ */
+ private String getPersonReference(ContactInfoCache.ContactCacheEntry contactInfo, Call call) {
+
+ String number = TelecomCallUtil.getNumber(call);
+ // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed.
+ // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid
+ // NotificationManager using it.
+ if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) {
+ return contactInfo.lookupUri.toString();
+ } else if (!TextUtils.isEmpty(number)) {
+ return Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null).toString();
+ }
+ return "";
+ }
+
+ private static class DialerCallDelegateStub implements DialerCallDelegate {
+
+ @Override
+ public DialerCall getDialerCallFromTelecomCall(Call telecomCall) {
+ return null;
+ }
+ }
+
+ /** Represents a call and associated cached notification data. */
+ private static class NotificationInfo {
+
+ @NonNull private final Call mCall;
+ private final int mNotificationId;
+ @Nullable private String mContentTitle;
+ @Nullable private Bitmap mLargeIcon;
+ @Nullable private String mPersonReference;
+
+ public NotificationInfo(@NonNull Call call, int notificationId) {
+ mCall = call;
+ mNotificationId = notificationId;
+ }
+
+ public Call getCall() {
+ return mCall;
+ }
+
+ public int getNotificationId() {
+ return mNotificationId;
+ }
+
+ public @Nullable String getContentTitle() {
+ return mContentTitle;
+ }
+
+ public void setContentTitle(@Nullable String contentTitle) {
+ mContentTitle = contentTitle;
+ }
+
+ public @Nullable Bitmap getLargeIcon() {
+ return mLargeIcon;
+ }
+
+ public void setLargeIcon(@Nullable Bitmap largeIcon) {
+ mLargeIcon = largeIcon;
+ }
+
+ public @Nullable String getPersonReference() {
+ return mPersonReference;
+ }
+
+ public void setPersonReference(@Nullable String personReference) {
+ mPersonReference = personReference;
+ }
+ }
+}
diff --git a/java/com/android/incallui/InCallActivity.java b/java/com/android/incallui/InCallActivity.java
new file mode 100644
index 000000000..307415916
--- /dev/null
+++ b/java/com/android/incallui/InCallActivity.java
@@ -0,0 +1,756 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.GradientDrawable.Orientation;
+import android.os.Bundle;
+import android.support.annotation.ColorInt;
+import android.support.annotation.FloatRange;
+import android.support.annotation.Nullable;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.graphics.ColorUtils;
+import android.telecom.DisconnectCause;
+import android.view.KeyEvent;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.ActivityCompat;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.ScreenEvent;
+import com.android.incallui.answer.bindings.AnswerBindings;
+import com.android.incallui.answer.protocol.AnswerScreen;
+import com.android.incallui.answer.protocol.AnswerScreenDelegate;
+import com.android.incallui.answer.protocol.AnswerScreenDelegateFactory;
+import com.android.incallui.answerproximitysensor.PseudoScreenState;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.State;
+import com.android.incallui.call.VideoUtils;
+import com.android.incallui.incall.bindings.InCallBindings;
+import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
+import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory;
+import com.android.incallui.incall.protocol.InCallScreen;
+import com.android.incallui.incall.protocol.InCallScreenDelegate;
+import com.android.incallui.incall.protocol.InCallScreenDelegateFactory;
+import com.android.incallui.video.bindings.VideoBindings;
+import com.android.incallui.video.protocol.VideoCallScreen;
+import com.android.incallui.video.protocol.VideoCallScreenDelegate;
+import com.android.incallui.video.protocol.VideoCallScreenDelegateFactory;
+
+/** Version of {@link InCallActivity} that shows the new UI */
+public class InCallActivity extends TransactionSafeFragmentActivity
+ implements AnswerScreenDelegateFactory,
+ InCallScreenDelegateFactory,
+ InCallButtonUiDelegateFactory,
+ VideoCallScreenDelegateFactory,
+ PseudoScreenState.StateChangedListener {
+
+ private static final String TAG_IN_CALL_SCREEN = "tag_in_call_screen";
+ private static final String TAG_ANSWER_SCREEN = "tag_answer_screen";
+ private static final String TAG_VIDEO_CALL_SCREEN = "tag_video_call_screen";
+
+ private static final String DID_SHOW_ANSWER_SCREEN_KEY = "did_show_answer_screen";
+ private static final String DID_SHOW_IN_CALL_SCREEN_KEY = "did_show_in_call_screen";
+ private static final String DID_SHOW_VIDEO_CALL_SCREEN_KEY = "did_show_video_call_screen";
+
+ private final InCallActivityCommon common;
+ private boolean didShowAnswerScreen;
+ private boolean didShowInCallScreen;
+ private boolean didShowVideoCallScreen;
+ private int[] backgroundDrawableColors;
+ private GradientDrawable backgroundDrawable;
+ private boolean isVisible;
+ private View pseudoBlackScreenOverlay;
+ private boolean touchDownWhenPseudoScreenOff;
+ private boolean isInShowMainInCallFragment;
+ private boolean needDismissPendingDialogs;
+
+ public InCallActivity() {
+ common = new InCallActivityCommon(this);
+ }
+
+ public static Intent getIntent(
+ Context context,
+ boolean showDialpad,
+ boolean newOutgoingCall,
+ boolean isVideoCall,
+ boolean isForFullScreen) {
+ Intent intent = new Intent(Intent.ACTION_MAIN, null);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION | Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setClass(context, InCallActivity.class);
+ InCallActivityCommon.setIntentExtras(intent, showDialpad, newOutgoingCall, isForFullScreen);
+ return intent;
+ }
+
+ @Override
+ protected void onResumeFragments() {
+ super.onResumeFragments();
+ if (needDismissPendingDialogs) {
+ dismissPendingDialogs();
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ LogUtil.i("InCallActivity.onCreate", "");
+ super.onCreate(icicle);
+
+ if (icicle != null) {
+ didShowAnswerScreen = icicle.getBoolean(DID_SHOW_ANSWER_SCREEN_KEY);
+ didShowInCallScreen = icicle.getBoolean(DID_SHOW_IN_CALL_SCREEN_KEY);
+ didShowVideoCallScreen = icicle.getBoolean(DID_SHOW_VIDEO_CALL_SCREEN_KEY);
+ }
+
+ common.onCreate(icicle);
+
+ getWindow()
+ .getDecorView()
+ .setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
+
+ pseudoBlackScreenOverlay = findViewById(R.id.psuedo_black_screen_overlay);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle out) {
+ LogUtil.i("InCallActivity.onSaveInstanceState", "");
+ common.onSaveInstanceState(out);
+ out.putBoolean(DID_SHOW_ANSWER_SCREEN_KEY, didShowAnswerScreen);
+ out.putBoolean(DID_SHOW_IN_CALL_SCREEN_KEY, didShowInCallScreen);
+ out.putBoolean(DID_SHOW_VIDEO_CALL_SCREEN_KEY, didShowVideoCallScreen);
+ super.onSaveInstanceState(out);
+ isVisible = false;
+ }
+
+ @Override
+ protected void onStart() {
+ LogUtil.i("InCallActivity.onStart", "");
+ super.onStart();
+ isVisible = true;
+ showMainInCallFragment();
+ common.onStart();
+ if (ActivityCompat.isInMultiWindowMode(this)
+ && !getResources().getBoolean(R.bool.incall_dialpad_allowed)) {
+ // Hide the dialpad because there may not be enough room
+ showDialpadFragment(false, false);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ LogUtil.i("InCallActivity.onResume", "");
+ super.onResume();
+ common.onResume();
+ PseudoScreenState pseudoScreenState = InCallPresenter.getInstance().getPseudoScreenState();
+ pseudoScreenState.addListener(this);
+ onPseudoScreenStateChanged(pseudoScreenState.isOn());
+ }
+
+ /** onPause is guaranteed to be called when the InCallActivity goes in the background. */
+ @Override
+ protected void onPause() {
+ LogUtil.i("InCallActivity.onPause", "");
+ super.onPause();
+ common.onPause();
+ InCallPresenter.getInstance().getPseudoScreenState().removeListener(this);
+ }
+
+ @Override
+ protected void onStop() {
+ LogUtil.i("InCallActivity.onStop", "");
+ super.onStop();
+ common.onStop();
+ isVisible = false;
+ }
+
+ @Override
+ protected void onDestroy() {
+ LogUtil.i("InCallActivity.onDestroy", "");
+ super.onDestroy();
+ common.onDestroy();
+ }
+
+ @Override
+ public void finish() {
+ if (shouldCloseActivityOnFinish()) {
+ super.finish();
+ }
+ }
+
+ private boolean shouldCloseActivityOnFinish() {
+ if (!isVisible()) {
+ LogUtil.i(
+ "InCallActivity.shouldCloseActivityOnFinish",
+ "allowing activity to be closed because it's not visible");
+ return true;
+ }
+
+ if (common.hasPendingDialogs()) {
+ LogUtil.i(
+ "InCallActivity.shouldCloseActivityOnFinish", "dialog is visible, not closing activity");
+ return false;
+ }
+
+ AnswerScreen answerScreen = getAnswerScreen();
+ if (answerScreen != null && answerScreen.hasPendingDialogs()) {
+ LogUtil.i(
+ "InCallActivity.shouldCloseActivityOnFinish",
+ "answer screen dialog is visible, not closing activity");
+ return false;
+ }
+
+ LogUtil.i(
+ "InCallActivity.shouldCloseActivityOnFinish",
+ "activity is visible and has no dialogs, allowing activity to close");
+ return true;
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ LogUtil.i("InCallActivity.onNewIntent", "");
+ common.onNewIntent(intent);
+
+ // If the screen is off, we need to make sure it gets turned on for incoming calls.
+ // This normally works just fine thanks to FLAG_TURN_SCREEN_ON but that only works
+ // when the activity is first created. Therefore, to ensure the screen is turned on
+ // for the call waiting case, we recreate() the current activity. There should be no jank from
+ // this since the screen is already off and will remain so until our new activity is up.
+ if (!isVisible()) {
+ LogUtil.i("InCallActivity.onNewIntent", "Restarting InCallActivity to force screen on.");
+ recreate();
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ LogUtil.i("InCallActivity.onBackPressed", "");
+ if (!common.onBackPressed(didShowInCallScreen || didShowVideoCallScreen)) {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ LogUtil.i("InCallActivity.onOptionsItemSelected", "item: " + item);
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (common.onKeyUp(keyCode, event)) {
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (common.onKeyDown(keyCode, event)) {
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ public boolean isInCallScreenAnimating() {
+ return false;
+ }
+
+ public void showConferenceFragment(boolean show) {
+ if (show) {
+ startActivity(new Intent(this, ManageConferenceActivity.class));
+ }
+ }
+
+ public boolean showDialpadFragment(boolean show, boolean animate) {
+ boolean didChange = common.showDialpadFragment(show, animate);
+ if (didChange) {
+ // Note: onInCallScreenDialpadVisibilityChange is called here to ensure that the dialpad FAB
+ // repositions itself.
+ getInCallScreen().onInCallScreenDialpadVisibilityChange(show);
+ }
+ return didChange;
+ }
+
+ public boolean isDialpadVisible() {
+ return common.isDialpadVisible();
+ }
+
+ public void onForegroundCallChanged(DialerCall newForegroundCall) {
+ common.updateTaskDescription();
+ if (didShowAnswerScreen && newForegroundCall != null) {
+ if (newForegroundCall.getState() == State.DISCONNECTED
+ || newForegroundCall.getState() == State.IDLE) {
+ LogUtil.i(
+ "InCallActivity.onForegroundCallChanged",
+ "rejecting incoming call, not updating " + "window background color");
+ }
+ } else {
+ LogUtil.v("InCallActivity.onForegroundCallChanged", "resetting background color");
+ updateWindowBackgroundColor(0);
+ }
+ }
+
+ public void updateWindowBackgroundColor(@FloatRange(from = -1f, to = 1.0f) float progress) {
+ ThemeColorManager themeColorManager = InCallPresenter.getInstance().getThemeColorManager();
+ @ColorInt int top;
+ @ColorInt int middle;
+ @ColorInt int bottom;
+ @ColorInt int gray = 0x66000000;
+
+ if (ActivityCompat.isInMultiWindowMode(this)) {
+ top = themeColorManager.getBackgroundColorSolid();
+ middle = themeColorManager.getBackgroundColorSolid();
+ bottom = themeColorManager.getBackgroundColorSolid();
+ } else {
+ top = themeColorManager.getBackgroundColorTop();
+ middle = themeColorManager.getBackgroundColorMiddle();
+ bottom = themeColorManager.getBackgroundColorBottom();
+ }
+
+ if (progress < 0) {
+ float correctedProgress = Math.abs(progress);
+ top = ColorUtils.blendARGB(top, gray, correctedProgress);
+ middle = ColorUtils.blendARGB(middle, gray, correctedProgress);
+ bottom = ColorUtils.blendARGB(bottom, gray, correctedProgress);
+ }
+
+ boolean backgroundDirty = false;
+ if (backgroundDrawable == null) {
+ backgroundDrawableColors = new int[] {top, middle, bottom};
+ backgroundDrawable = new GradientDrawable(Orientation.TOP_BOTTOM, backgroundDrawableColors);
+ backgroundDirty = true;
+ } else {
+ if (backgroundDrawableColors[0] != top) {
+ backgroundDrawableColors[0] = top;
+ backgroundDirty = true;
+ }
+ if (backgroundDrawableColors[1] != middle) {
+ backgroundDrawableColors[1] = middle;
+ backgroundDirty = true;
+ }
+ if (backgroundDrawableColors[2] != bottom) {
+ backgroundDrawableColors[2] = bottom;
+ backgroundDirty = true;
+ }
+ if (backgroundDirty) {
+ backgroundDrawable.setColors(backgroundDrawableColors);
+ }
+ }
+
+ if (backgroundDirty) {
+ getWindow().setBackgroundDrawable(backgroundDrawable);
+ }
+ }
+
+ public boolean isVisible() {
+ return isVisible;
+ }
+
+ public boolean getCallCardFragmentVisible() {
+ return didShowInCallScreen || didShowVideoCallScreen;
+ }
+
+ public void dismissKeyguard(boolean dismiss) {
+ common.dismissKeyguard(dismiss);
+ }
+
+ public void showPostCharWaitDialog(String callId, String chars) {
+ common.showPostCharWaitDialog(callId, chars);
+ }
+
+ public void maybeShowErrorDialogOnDisconnect(DisconnectCause disconnectCause) {
+ common.maybeShowErrorDialogOnDisconnect(disconnectCause);
+ }
+
+ public void dismissPendingDialogs() {
+ if (isVisible) {
+ LogUtil.i("InCallActivity.dismissPendingDialogs", "");
+ common.dismissPendingDialogs();
+ AnswerScreen answerScreen = getAnswerScreen();
+ if (answerScreen != null) {
+ answerScreen.dismissPendingDialogs();
+ }
+ needDismissPendingDialogs = false;
+ } else {
+ // The activity is not visible and onSaveInstanceState may have been called so defer the
+ // dismissing action.
+ LogUtil.i(
+ "InCallActivity.dismissPendingDialogs", "defer actions since activity is not visible");
+ needDismissPendingDialogs = true;
+ }
+ }
+
+ private void enableInCallOrientationEventListener(boolean enable) {
+ common.enableInCallOrientationEventListener(enable);
+ }
+
+ public void setExcludeFromRecents(boolean exclude) {
+ common.setExcludeFromRecents(exclude);
+ }
+
+ public void onResolveIntent(
+ DialerCall outgoingCall, boolean isNewOutgoingCall, boolean didShowAccountSelectionDialog) {
+ if (didShowAccountSelectionDialog) {
+ hideMainInCallFragment();
+ }
+ }
+
+ @Nullable
+ public FragmentManager getDialpadFragmentManager() {
+ InCallScreen inCallScreen = getInCallScreen();
+ if (inCallScreen != null) {
+ return inCallScreen.getInCallScreenFragment().getChildFragmentManager();
+ }
+ return null;
+ }
+
+ public int getDialpadContainerId() {
+ return getInCallScreen().getAnswerAndDialpadContainerResourceId();
+ }
+
+ @Override
+ public AnswerScreenDelegate newAnswerScreenDelegate(AnswerScreen answerScreen) {
+ DialerCall call = CallList.getInstance().getCallById(answerScreen.getCallId());
+ if (call == null) {
+ // This is a work around for a bug where we attempt to create a new delegate after the call
+ // has already been removed. An example of when this can happen is:
+ // 1. incoming video call in landscape mode
+ // 2. remote party hangs up
+ // 3. activity switches from landscape to portrait
+ // At step #3 the answer fragment will try to create a new answer delegate but the call won't
+ // exist. In this case we'll simply return a stub delegate that does nothing. This is ok
+ // because this new state is transient and the activity will be destroyed soon.
+ LogUtil.i("InCallActivity.onPrimaryCallStateChanged", "call doesn't exist, using stub");
+ return new AnswerScreenPresenterStub();
+ } else {
+ return new AnswerScreenPresenter(
+ this, answerScreen, CallList.getInstance().getCallById(answerScreen.getCallId()));
+ }
+ }
+
+ @Override
+ public InCallScreenDelegate newInCallScreenDelegate() {
+ return new CallCardPresenter(this);
+ }
+
+ @Override
+ public InCallButtonUiDelegate newInCallButtonUiDelegate() {
+ return new CallButtonPresenter(this);
+ }
+
+ @Override
+ public VideoCallScreenDelegate newVideoCallScreenDelegate() {
+ return new VideoCallPresenter();
+ }
+
+ public void onPrimaryCallStateChanged() {
+ LogUtil.i("InCallActivity.onPrimaryCallStateChanged", "");
+ showMainInCallFragment();
+ }
+
+ public void onWiFiToLteHandover(DialerCall call) {
+ common.showWifiToLteHandoverToast(call);
+ }
+
+ public void onHandoverToWifiFailed(DialerCall call) {
+ common.showWifiFailedDialog(call);
+ }
+
+ public void setAllowOrientationChange(boolean allowOrientationChange) {
+ if (!allowOrientationChange) {
+ setRequestedOrientation(InCallOrientationEventListener.ACTIVITY_PREFERENCE_DISALLOW_ROTATION);
+ } else {
+ setRequestedOrientation(InCallOrientationEventListener.ACTIVITY_PREFERENCE_ALLOW_ROTATION);
+ }
+ enableInCallOrientationEventListener(allowOrientationChange);
+ }
+
+ private void hideMainInCallFragment() {
+ LogUtil.i("InCallActivity.hideMainInCallFragment", "");
+ if (didShowInCallScreen || didShowVideoCallScreen) {
+ FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
+ hideInCallScreenFragment(transaction);
+ hideVideoCallScreenFragment(transaction);
+ transaction.commitAllowingStateLoss();
+ getSupportFragmentManager().executePendingTransactions();
+ }
+ }
+
+ private void showMainInCallFragment() {
+ // If the activity's onStart method hasn't been called yet then defer doing any work.
+ if (!isVisible) {
+ LogUtil.i("InCallActivity.showMainInCallFragment", "not visible yet/anymore");
+ return;
+ }
+
+ // Don't let this be reentrant.
+ if (isInShowMainInCallFragment) {
+ LogUtil.i("InCallActivity.showMainInCallFragment", "already in method, bailing");
+ return;
+ }
+
+ isInShowMainInCallFragment = true;
+ ShouldShowAnswerUiResult shouldShowAnswerUi = getShouldShowAnswerUi();
+ boolean shouldShowVideoUi = getShouldShowVideoUi();
+ LogUtil.i(
+ "InCallActivity.showMainInCallFragment",
+ "shouldShowAnswerUi: %b, shouldShowVideoUi: %b, "
+ + "didShowAnswerScreen: %b, didShowInCallScreen: %b, didShowVideoCallScreen: %b",
+ shouldShowAnswerUi.shouldShow,
+ shouldShowVideoUi,
+ didShowAnswerScreen,
+ didShowInCallScreen,
+ didShowVideoCallScreen);
+ // Only video call ui allows orientation change.
+ setAllowOrientationChange(shouldShowVideoUi);
+
+ FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
+ boolean didChangeInCall;
+ boolean didChangeVideo;
+ boolean didChangeAnswer;
+ if (shouldShowAnswerUi.shouldShow) {
+ didChangeInCall = hideInCallScreenFragment(transaction);
+ didChangeVideo = hideVideoCallScreenFragment(transaction);
+ didChangeAnswer = showAnswerScreenFragment(transaction, shouldShowAnswerUi.call);
+ } else if (shouldShowVideoUi) {
+ didChangeInCall = hideInCallScreenFragment(transaction);
+ didChangeVideo = showVideoCallScreenFragment(transaction);
+ didChangeAnswer = hideAnswerScreenFragment(transaction);
+ } else {
+ didChangeInCall = showInCallScreenFragment(transaction);
+ didChangeVideo = hideVideoCallScreenFragment(transaction);
+ didChangeAnswer = hideAnswerScreenFragment(transaction);
+ }
+
+ if (didChangeInCall || didChangeVideo || didChangeAnswer) {
+ transaction.commitNow();
+ Logger.get(this).logScreenView(ScreenEvent.Type.INCALL, this);
+ }
+ isInShowMainInCallFragment = false;
+ }
+
+ private ShouldShowAnswerUiResult getShouldShowAnswerUi() {
+ DialerCall call = CallList.getInstance().getIncomingCall();
+ if (call != null) {
+ LogUtil.i("InCallActivity.getShouldShowAnswerUi", "found incoming call");
+ return new ShouldShowAnswerUiResult(true, call);
+ }
+
+ call = CallList.getInstance().getVideoUpgradeRequestCall();
+ if (call != null) {
+ LogUtil.i("InCallActivity.getShouldShowAnswerUi", "found video upgrade request");
+ return new ShouldShowAnswerUiResult(true, call);
+ }
+
+ // Check if we're showing the answer screen and the call is disconnected. If this condition is
+ // true then we won't switch from the answer UI to the in call UI. This prevents flicker when
+ // the user rejects an incoming call.
+ call = CallList.getInstance().getFirstCall();
+ if (call == null) {
+ call = CallList.getInstance().getBackgroundCall();
+ }
+ if (didShowAnswerScreen && (call == null || call.getState() == State.DISCONNECTED)) {
+ LogUtil.i("InCallActivity.getShouldShowAnswerUi", "found disconnecting incoming call");
+ return new ShouldShowAnswerUiResult(true, call);
+ }
+
+ return new ShouldShowAnswerUiResult(false, null);
+ }
+
+ private boolean getShouldShowVideoUi() {
+ DialerCall call = CallList.getInstance().getFirstCall();
+ if (call == null) {
+ LogUtil.i("InCallActivity.getShouldShowVideoUi", "null call");
+ return false;
+ }
+
+ if (VideoUtils.isVideoCall(call)) {
+ LogUtil.i("InCallActivity.getShouldShowVideoUi", "found video call");
+ return true;
+ }
+
+ if (VideoUtils.hasSentVideoUpgradeRequest(call)) {
+ LogUtil.i("InCallActivity.getShouldShowVideoUi", "upgrading to video");
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean showAnswerScreenFragment(FragmentTransaction transaction, DialerCall call) {
+ // When rejecting a call the active call can become null in which case we should continue
+ // showing the answer screen.
+ if (didShowAnswerScreen && call == null) {
+ return false;
+ }
+
+ boolean isVideoUpgradeRequest = VideoUtils.hasReceivedVideoUpgradeRequest(call);
+ int videoState = isVideoUpgradeRequest ? call.getRequestedVideoState() : call.getVideoState();
+
+ // Check if we're already showing an answer screen for this call.
+ if (didShowAnswerScreen) {
+ AnswerScreen answerScreen = getAnswerScreen();
+ if (answerScreen.getCallId().equals(call.getId())
+ && answerScreen.getVideoState() == videoState
+ && answerScreen.isVideoUpgradeRequest() == isVideoUpgradeRequest) {
+ return false;
+ }
+ LogUtil.i(
+ "InCallActivity.showAnswerScreenFragment",
+ "answer fragment exists but arguments do not match");
+ hideAnswerScreenFragment(transaction);
+ }
+
+ // Show a new answer screen.
+ AnswerScreen answerScreen =
+ AnswerBindings.createAnswerScreen(call.getId(), videoState, isVideoUpgradeRequest);
+ transaction.add(R.id.main, answerScreen.getAnswerScreenFragment(), TAG_ANSWER_SCREEN);
+
+ Logger.get(this).logScreenView(ScreenEvent.Type.INCOMING_CALL, this);
+ didShowAnswerScreen = true;
+ return true;
+ }
+
+ private boolean hideAnswerScreenFragment(FragmentTransaction transaction) {
+ if (!didShowAnswerScreen) {
+ return false;
+ }
+ AnswerScreen answerScreen = getAnswerScreen();
+ if (answerScreen != null) {
+ transaction.remove(answerScreen.getAnswerScreenFragment());
+ }
+
+ didShowAnswerScreen = false;
+ return true;
+ }
+
+ private boolean showInCallScreenFragment(FragmentTransaction transaction) {
+ if (didShowInCallScreen) {
+ return false;
+ }
+ InCallScreen inCallScreen = getInCallScreen();
+ if (inCallScreen == null) {
+ inCallScreen = InCallBindings.createInCallScreen();
+ transaction.add(R.id.main, inCallScreen.getInCallScreenFragment(), TAG_IN_CALL_SCREEN);
+ } else {
+ transaction.show(inCallScreen.getInCallScreenFragment());
+ }
+ Logger.get(this).logScreenView(ScreenEvent.Type.INCALL, this);
+ didShowInCallScreen = true;
+ return true;
+ }
+
+ private boolean hideInCallScreenFragment(FragmentTransaction transaction) {
+ if (!didShowInCallScreen) {
+ return false;
+ }
+ InCallScreen inCallScreen = getInCallScreen();
+ if (inCallScreen != null) {
+ transaction.hide(inCallScreen.getInCallScreenFragment());
+ }
+ didShowInCallScreen = false;
+ return true;
+ }
+
+ private boolean showVideoCallScreenFragment(FragmentTransaction transaction) {
+ if (didShowVideoCallScreen) {
+ return false;
+ }
+
+ VideoCallScreen videoCallScreen = VideoBindings.createVideoCallScreen();
+ transaction.add(R.id.main, videoCallScreen.getVideoCallScreenFragment(), TAG_VIDEO_CALL_SCREEN);
+
+ Logger.get(this).logScreenView(ScreenEvent.Type.INCALL, this);
+ didShowVideoCallScreen = true;
+ return true;
+ }
+
+ private boolean hideVideoCallScreenFragment(FragmentTransaction transaction) {
+ if (!didShowVideoCallScreen) {
+ return false;
+ }
+ VideoCallScreen videoCallScreen = getVideoCallScreen();
+ if (videoCallScreen != null) {
+ transaction.remove(videoCallScreen.getVideoCallScreenFragment());
+ }
+ didShowVideoCallScreen = false;
+ return true;
+ }
+
+ AnswerScreen getAnswerScreen() {
+ return (AnswerScreen) getSupportFragmentManager().findFragmentByTag(TAG_ANSWER_SCREEN);
+ }
+
+ InCallScreen getInCallScreen() {
+ return (InCallScreen) getSupportFragmentManager().findFragmentByTag(TAG_IN_CALL_SCREEN);
+ }
+
+ VideoCallScreen getVideoCallScreen() {
+ return (VideoCallScreen) getSupportFragmentManager().findFragmentByTag(TAG_VIDEO_CALL_SCREEN);
+ }
+
+ @Override
+ public void onPseudoScreenStateChanged(boolean isOn) {
+ LogUtil.i("InCallActivity.onPseudoScreenStateChanged", "isOn: " + isOn);
+ pseudoBlackScreenOverlay.setVisibility(isOn ? View.GONE : View.VISIBLE);
+ }
+
+ /**
+ * For some touch related issue, turning off the screen can be faked by drawing a black view over
+ * the activity. All touch events started when the screen is "off" is rejected.
+ *
+ * @see PseudoScreenState
+ */
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ // Reject any gesture that started when the screen is in the fake off state.
+ if (touchDownWhenPseudoScreenOff) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ touchDownWhenPseudoScreenOff = false;
+ }
+ return true;
+ }
+ // Reject all touch event when the screen is in the fake off state.
+ if (!InCallPresenter.getInstance().getPseudoScreenState().isOn()) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ touchDownWhenPseudoScreenOff = true;
+ LogUtil.i("InCallActivity.dispatchTouchEvent", "touchDownWhenPseudoScreenOff");
+ }
+ return true;
+ }
+ return super.dispatchTouchEvent(event);
+ }
+
+ private static class ShouldShowAnswerUiResult {
+ public final boolean shouldShow;
+ public final DialerCall call;
+
+ ShouldShowAnswerUiResult(boolean shouldShow, DialerCall call) {
+ this.shouldShow = shouldShow;
+ this.call = call;
+ }
+ }
+}
diff --git a/java/com/android/incallui/InCallActivityCommon.java b/java/com/android/incallui/InCallActivityCommon.java
new file mode 100644
index 000000000..a2467dd72
--- /dev/null
+++ b/java/com/android/incallui/InCallActivityCommon.java
@@ -0,0 +1,820 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui;
+
+import android.app.ActivityManager;
+import android.app.ActivityManager.AppTask;
+import android.app.ActivityManager.TaskDescription;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnDismissListener;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.content.res.ResourcesCompat;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import android.util.Pair;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.CheckBox;
+import android.widget.Toast;
+import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment;
+import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment.SelectPhoneAccountListener;
+import com.android.dialer.animation.AnimUtils;
+import com.android.dialer.animation.AnimationListenerAdapter;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.ScreenEvent;
+import com.android.dialer.util.ViewUtil;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.State;
+import com.android.incallui.call.TelecomAdapter;
+import com.android.incallui.wifi.EnableWifiCallingPrompt;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Shared functionality between the new and old in call activity. */
+public class InCallActivityCommon {
+
+ private static final String INTENT_EXTRA_SHOW_DIALPAD = "InCallActivity.show_dialpad";
+ private static final String INTENT_EXTRA_NEW_OUTGOING_CALL = "InCallActivity.new_outgoing_call";
+ private static final String INTENT_EXTRA_FOR_FULL_SCREEN =
+ "InCallActivity.for_full_screen_intent";
+
+ private static final String DIALPAD_TEXT_KEY = "InCallActivity.dialpad_text";
+
+ private static final String TAG_SELECT_ACCOUNT_FRAGMENT = "tag_select_account_fragment";
+ private static final String TAG_DIALPAD_FRAGMENT = "tag_dialpad_fragment";
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ DIALPAD_REQUEST_NONE,
+ DIALPAD_REQUEST_SHOW,
+ DIALPAD_REQUEST_HIDE,
+ })
+ @interface DialpadRequestType {}
+
+ private static final int DIALPAD_REQUEST_NONE = 1;
+ private static final int DIALPAD_REQUEST_SHOW = 2;
+ private static final int DIALPAD_REQUEST_HIDE = 3;
+
+ private final InCallActivity inCallActivity;
+ private boolean dismissKeyguard;
+ private boolean showPostCharWaitDialogOnResume;
+ private String showPostCharWaitDialogCallId;
+ private String showPostCharWaitDialogChars;
+ private Dialog dialog;
+ private InCallOrientationEventListener inCallOrientationEventListener;
+ private Animation dialpadSlideInAnimation;
+ private Animation dialpadSlideOutAnimation;
+ private boolean animateDialpadOnShow;
+ private String dtmfTextToPreopulate;
+ @DialpadRequestType private int showDialpadRequest = DIALPAD_REQUEST_NONE;
+
+ private SelectPhoneAccountListener selectAccountListener =
+ new SelectPhoneAccountListener() {
+ @Override
+ public void onPhoneAccountSelected(
+ PhoneAccountHandle selectedAccountHandle, boolean setDefault, String callId) {
+ DialerCall call = CallList.getInstance().getCallById(callId);
+ LogUtil.i(
+ "InCallActivityCommon.SelectPhoneAccountListener.onPhoneAccountSelected",
+ "call: " + call);
+ if (call != null) {
+ call.phoneAccountSelected(selectedAccountHandle, setDefault);
+ }
+ }
+
+ @Override
+ public void onDialogDismissed(String callId) {
+ DialerCall call = CallList.getInstance().getCallById(callId);
+ LogUtil.i(
+ "InCallActivityCommon.SelectPhoneAccountListener.onDialogDismissed",
+ "disconnecting call: " + call);
+ if (call != null) {
+ call.disconnect();
+ }
+ }
+ };
+
+ public static void setIntentExtras(
+ Intent intent, boolean showDialpad, boolean newOutgoingCall, boolean isForFullScreen) {
+ if (showDialpad) {
+ intent.putExtra(INTENT_EXTRA_SHOW_DIALPAD, true);
+ }
+ intent.putExtra(INTENT_EXTRA_NEW_OUTGOING_CALL, newOutgoingCall);
+ intent.putExtra(INTENT_EXTRA_FOR_FULL_SCREEN, isForFullScreen);
+ }
+
+ public InCallActivityCommon(InCallActivity inCallActivity) {
+ this.inCallActivity = inCallActivity;
+ }
+
+ public void onCreate(Bundle icicle) {
+ // set this flag so this activity will stay in front of the keyguard
+ // Have the WindowManager filter out touch events that are "too fat".
+ int flags =
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+ | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES;
+
+ inCallActivity.getWindow().addFlags(flags);
+
+ inCallActivity.setContentView(R.layout.incall_screen);
+
+ internalResolveIntent(inCallActivity.getIntent());
+
+ boolean isLandscape =
+ inCallActivity.getResources().getConfiguration().orientation
+ == Configuration.ORIENTATION_LANDSCAPE;
+ boolean isRtl = ViewUtil.isRtl();
+
+ if (isLandscape) {
+ dialpadSlideInAnimation =
+ AnimationUtils.loadAnimation(
+ inCallActivity, isRtl ? R.anim.dialpad_slide_in_left : R.anim.dialpad_slide_in_right);
+ dialpadSlideOutAnimation =
+ AnimationUtils.loadAnimation(
+ inCallActivity,
+ isRtl ? R.anim.dialpad_slide_out_left : R.anim.dialpad_slide_out_right);
+ } else {
+ dialpadSlideInAnimation =
+ AnimationUtils.loadAnimation(inCallActivity, R.anim.dialpad_slide_in_bottom);
+ dialpadSlideOutAnimation =
+ AnimationUtils.loadAnimation(inCallActivity, R.anim.dialpad_slide_out_bottom);
+ }
+
+ dialpadSlideInAnimation.setInterpolator(AnimUtils.EASE_IN);
+ dialpadSlideOutAnimation.setInterpolator(AnimUtils.EASE_OUT);
+
+ dialpadSlideOutAnimation.setAnimationListener(
+ new AnimationListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ performHideDialpadFragment();
+ }
+ });
+
+ if (icicle != null) {
+ // If the dialpad was shown before, set variables indicating it should be shown and
+ // populated with the previous DTMF text. The dialpad is actually shown and populated
+ // in onResume() to ensure the hosting fragment has been inflated and is ready to receive it.
+ if (icicle.containsKey(INTENT_EXTRA_SHOW_DIALPAD)) {
+ boolean showDialpad = icicle.getBoolean(INTENT_EXTRA_SHOW_DIALPAD);
+ showDialpadRequest = showDialpad ? DIALPAD_REQUEST_SHOW : DIALPAD_REQUEST_HIDE;
+ animateDialpadOnShow = false;
+ }
+ dtmfTextToPreopulate = icicle.getString(DIALPAD_TEXT_KEY);
+
+ SelectPhoneAccountDialogFragment dialogFragment =
+ (SelectPhoneAccountDialogFragment)
+ inCallActivity.getFragmentManager().findFragmentByTag(TAG_SELECT_ACCOUNT_FRAGMENT);
+ if (dialogFragment != null) {
+ dialogFragment.setListener(selectAccountListener);
+ }
+ }
+
+ inCallOrientationEventListener = new InCallOrientationEventListener(inCallActivity);
+ }
+
+ public void onSaveInstanceState(Bundle out) {
+ // TODO: The dialpad fragment should handle this as part of its own state
+ out.putBoolean(INTENT_EXTRA_SHOW_DIALPAD, isDialpadVisible());
+ DialpadFragment dialpadFragment = getDialpadFragment();
+ if (dialpadFragment != null) {
+ out.putString(DIALPAD_TEXT_KEY, dialpadFragment.getDtmfText());
+ }
+ }
+
+ public void onStart() {
+ // setting activity should be last thing in setup process
+ InCallPresenter.getInstance().setActivity(inCallActivity);
+ enableInCallOrientationEventListener(
+ inCallActivity.getRequestedOrientation()
+ == InCallOrientationEventListener.ACTIVITY_PREFERENCE_ALLOW_ROTATION);
+
+ InCallPresenter.getInstance().onActivityStarted();
+ }
+
+ public void onResume() {
+ if (InCallPresenter.getInstance().isReadyForTearDown()) {
+ LogUtil.i(
+ "InCallActivityCommon.onResume",
+ "InCallPresenter is ready for tear down, not sending updates");
+ } else {
+ updateTaskDescription();
+ InCallPresenter.getInstance().onUiShowing(true);
+ }
+
+ // If there is a pending request to show or hide the dialpad, handle that now.
+ if (showDialpadRequest != DIALPAD_REQUEST_NONE) {
+ if (showDialpadRequest == DIALPAD_REQUEST_SHOW) {
+ // Exit fullscreen so that the user has access to the dialpad hide/show button and
+ // can hide the dialpad. Important when showing the dialpad from within dialer.
+ InCallPresenter.getInstance().setFullScreen(false, true /* force */);
+
+ inCallActivity.showDialpadFragment(true /* show */, animateDialpadOnShow /* animate */);
+ animateDialpadOnShow = false;
+
+ DialpadFragment dialpadFragment = getDialpadFragment();
+ if (dialpadFragment != null) {
+ dialpadFragment.setDtmfText(dtmfTextToPreopulate);
+ dtmfTextToPreopulate = null;
+ }
+ } else {
+ LogUtil.i("InCallActivityCommon.onResume", "force hide dialpad");
+ if (getDialpadFragment() != null) {
+ inCallActivity.showDialpadFragment(false /* show */, false /* animate */);
+ }
+ }
+ showDialpadRequest = DIALPAD_REQUEST_NONE;
+ }
+
+ if (showPostCharWaitDialogOnResume) {
+ showPostCharWaitDialog(showPostCharWaitDialogCallId, showPostCharWaitDialogChars);
+ }
+
+ CallList.getInstance()
+ .onInCallUiShown(
+ inCallActivity.getIntent().getBooleanExtra(INTENT_EXTRA_FOR_FULL_SCREEN, false));
+ }
+
+ // onPause is guaranteed to be called when the InCallActivity goes
+ // in the background.
+ public void onPause() {
+ DialpadFragment dialpadFragment = getDialpadFragment();
+ if (dialpadFragment != null) {
+ dialpadFragment.onDialerKeyUp(null);
+ }
+
+ InCallPresenter.getInstance().onUiShowing(false);
+ if (inCallActivity.isFinishing()) {
+ InCallPresenter.getInstance().unsetActivity(inCallActivity);
+ }
+ }
+
+ public void onStop() {
+ enableInCallOrientationEventListener(false);
+ InCallPresenter.getInstance().updateIsChangingConfigurations();
+ InCallPresenter.getInstance().onActivityStopped();
+ }
+
+ public void onDestroy() {
+ InCallPresenter.getInstance().unsetActivity(inCallActivity);
+ InCallPresenter.getInstance().updateIsChangingConfigurations();
+ }
+
+ public void onNewIntent(Intent intent) {
+ LogUtil.i("InCallActivityCommon.onNewIntent", "");
+
+ // We're being re-launched with a new Intent. Since it's possible for a
+ // single InCallActivity instance to persist indefinitely (even if we
+ // finish() ourselves), this sequence can potentially happen any time
+ // the InCallActivity needs to be displayed.
+
+ // Stash away the new intent so that we can get it in the future
+ // by calling getIntent(). (Otherwise getIntent() will return the
+ // original Intent from when we first got created!)
+ inCallActivity.setIntent(intent);
+
+ // Activities are always paused before receiving a new intent, so
+ // we can count on our onResume() method being called next.
+
+ // Just like in onCreate(), handle the intent.
+ internalResolveIntent(intent);
+ }
+
+ public boolean onBackPressed(boolean isInCallScreenVisible) {
+ LogUtil.i("InCallActivityCommon.onBackPressed", "");
+
+ // BACK is also used to exit out of any "special modes" of the
+ // in-call UI:
+ if (!inCallActivity.isVisible()) {
+ return true;
+ }
+
+ if (!isInCallScreenVisible) {
+ return true;
+ }
+
+ DialpadFragment dialpadFragment = getDialpadFragment();
+ if (dialpadFragment != null && dialpadFragment.isVisible()) {
+ inCallActivity.showDialpadFragment(false /* show */, true /* animate */);
+ return true;
+ }
+
+ // Always disable the Back key while an incoming call is ringing
+ DialerCall call = CallList.getInstance().getIncomingCall();
+ if (call != null) {
+ LogUtil.i("InCallActivityCommon.onBackPressed", "consume Back press for an incoming call");
+ return true;
+ }
+
+ // Nothing special to do. Fall back to the default behavior.
+ return false;
+ }
+
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ DialpadFragment dialpadFragment = getDialpadFragment();
+ // push input to the dialer.
+ if (dialpadFragment != null
+ && (dialpadFragment.isVisible())
+ && (dialpadFragment.onDialerKeyUp(event))) {
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_CALL) {
+ // Always consume CALL to be sure the PhoneWindow won't do anything with it
+ return true;
+ }
+ return false;
+ }
+
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_CALL:
+ boolean handled = InCallPresenter.getInstance().handleCallKey();
+ if (!handled) {
+ LogUtil.e(
+ "InCallActivityCommon.onKeyDown",
+ "InCallPresenter should always handle KEYCODE_CALL in onKeyDown");
+ }
+ // Always consume CALL to be sure the PhoneWindow won't do anything with it
+ return true;
+
+ // Note there's no KeyEvent.KEYCODE_ENDCALL case here.
+ // The standard system-wide handling of the ENDCALL key
+ // (see PhoneWindowManager's handling of KEYCODE_ENDCALL)
+ // already implements exactly what the UI spec wants,
+ // namely (1) "hang up" if there's a current active call,
+ // or (2) "don't answer" if there's a current ringing call.
+
+ case KeyEvent.KEYCODE_CAMERA:
+ // Disable the CAMERA button while in-call since it's too
+ // easy to press accidentally.
+ return true;
+
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ case KeyEvent.KEYCODE_VOLUME_MUTE:
+ // Ringer silencing handled by PhoneWindowManager.
+ break;
+
+ case KeyEvent.KEYCODE_MUTE:
+ TelecomAdapter.getInstance()
+ .mute(!AudioModeProvider.getInstance().getAudioState().isMuted());
+ return true;
+
+ // Various testing/debugging features, enabled ONLY when VERBOSE == true.
+ case KeyEvent.KEYCODE_SLASH:
+ if (LogUtil.isVerboseEnabled()) {
+ LogUtil.v(
+ "InCallActivityCommon.onKeyDown",
+ "----------- InCallActivity View dump --------------");
+ // Dump starting from the top-level view of the entire activity:
+ Window w = inCallActivity.getWindow();
+ View decorView = w.getDecorView();
+ LogUtil.v("InCallActivityCommon.onKeyDown", "View dump:" + decorView);
+ return true;
+ }
+ break;
+ case KeyEvent.KEYCODE_EQUALS:
+ break;
+ }
+
+ return event.getRepeatCount() == 0 && handleDialerKeyDown(keyCode, event);
+ }
+
+ private boolean handleDialerKeyDown(int keyCode, KeyEvent event) {
+ LogUtil.v("InCallActivityCommon.handleDialerKeyDown", "keyCode %d, event: %s", keyCode, event);
+
+ // As soon as the user starts typing valid dialable keys on the
+ // keyboard (presumably to type DTMF tones) we start passing the
+ // key events to the DTMFDialer's onDialerKeyDown.
+ DialpadFragment dialpadFragment = getDialpadFragment();
+ if (dialpadFragment != null && dialpadFragment.isVisible()) {
+ return dialpadFragment.onDialerKeyDown(event);
+ }
+
+ return false;
+ }
+
+ public void dismissKeyguard(boolean dismiss) {
+ if (dismissKeyguard == dismiss) {
+ return;
+ }
+ dismissKeyguard = dismiss;
+ if (dismiss) {
+ inCallActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+ } else {
+ inCallActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+ }
+ }
+
+ public void showPostCharWaitDialog(String callId, String chars) {
+ if (inCallActivity.isVisible()) {
+ PostCharDialogFragment fragment = new PostCharDialogFragment(callId, chars);
+ fragment.show(inCallActivity.getSupportFragmentManager(), "postCharWait");
+
+ showPostCharWaitDialogOnResume = false;
+ showPostCharWaitDialogCallId = null;
+ showPostCharWaitDialogChars = null;
+ } else {
+ showPostCharWaitDialogOnResume = true;
+ showPostCharWaitDialogCallId = callId;
+ showPostCharWaitDialogChars = chars;
+ }
+ }
+
+ public void maybeShowErrorDialogOnDisconnect(DisconnectCause cause) {
+ LogUtil.i(
+ "InCallActivityCommon.maybeShowErrorDialogOnDisconnect", "disconnect cause: %s", cause);
+
+ if (!inCallActivity.isFinishing()) {
+ if (EnableWifiCallingPrompt.shouldShowPrompt(cause)) {
+ Pair<Dialog, CharSequence> pair =
+ EnableWifiCallingPrompt.createDialog(inCallActivity, cause);
+ showErrorDialog(pair.first, pair.second);
+ } else if (shouldShowDisconnectErrorDialog(cause)) {
+ Pair<Dialog, CharSequence> pair = getDisconnectErrorDialog(inCallActivity, cause);
+ showErrorDialog(pair.first, pair.second);
+ }
+ }
+ }
+
+ /**
+ * When relaunching from the dialer app, {@code showDialpad} indicates whether the dialpad should
+ * be shown on launch.
+ *
+ * @param showDialpad {@code true} to indicate the dialpad should be shown on launch, and {@code
+ * false} to indicate no change should be made to the dialpad visibility.
+ */
+ private void relaunchedFromDialer(boolean showDialpad) {
+ showDialpadRequest = showDialpad ? DIALPAD_REQUEST_SHOW : DIALPAD_REQUEST_NONE;
+ animateDialpadOnShow = true;
+
+ if (showDialpadRequest == DIALPAD_REQUEST_SHOW) {
+ // If there's only one line in use, AND it's on hold, then we're sure the user
+ // wants to use the dialpad toward the exact line, so un-hold the holding line.
+ DialerCall call = CallList.getInstance().getActiveOrBackgroundCall();
+ if (call != null && call.getState() == State.ONHOLD) {
+ call.unhold();
+ }
+ }
+ }
+
+ public void dismissPendingDialogs() {
+ if (dialog != null) {
+ dialog.dismiss();
+ dialog = null;
+ }
+ }
+
+ private static boolean shouldShowDisconnectErrorDialog(@NonNull DisconnectCause cause) {
+ return !TextUtils.isEmpty(cause.getDescription())
+ && (cause.getCode() == DisconnectCause.ERROR
+ || cause.getCode() == DisconnectCause.RESTRICTED);
+ }
+
+ private static Pair<Dialog, CharSequence> getDisconnectErrorDialog(
+ @NonNull Context context, @NonNull DisconnectCause cause) {
+ CharSequence message = cause.getDescription();
+ Dialog dialog =
+ new AlertDialog.Builder(context)
+ .setMessage(message)
+ .setPositiveButton(android.R.string.ok, null)
+ .create();
+ return new Pair<>(dialog, message);
+ }
+
+ private void showErrorDialog(Dialog dialog, CharSequence message) {
+ LogUtil.i("InCallActivityCommon.showErrorDialog", "message: %s", message);
+ inCallActivity.dismissPendingDialogs();
+
+ // Show toast if apps is in background when dialog won't be visible.
+ if (!inCallActivity.isVisible()) {
+ Toast.makeText(inCallActivity.getApplicationContext(), message, Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ this.dialog = dialog;
+ dialog.setOnDismissListener(
+ new OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ LogUtil.i("InCallActivityCommon.showErrorDialog", "dialog dismissed");
+ onDialogDismissed();
+ }
+ });
+ dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+ dialog.show();
+ }
+
+ private void onDialogDismissed() {
+ dialog = null;
+ CallList.getInstance().onErrorDialogDismissed();
+ InCallPresenter.getInstance().onDismissDialog();
+ }
+
+ public void enableInCallOrientationEventListener(boolean enable) {
+ if (enable) {
+ inCallOrientationEventListener.enable(true);
+ } else {
+ inCallOrientationEventListener.disable();
+ }
+ }
+
+ public void setExcludeFromRecents(boolean exclude) {
+ List<AppTask> tasks = inCallActivity.getSystemService(ActivityManager.class).getAppTasks();
+ int taskId = inCallActivity.getTaskId();
+ for (int i = 0; i < tasks.size(); i++) {
+ ActivityManager.AppTask task = tasks.get(i);
+ try {
+ if (task.getTaskInfo().id == taskId) {
+ task.setExcludeFromRecents(exclude);
+ }
+ } catch (RuntimeException e) {
+ LogUtil.e(
+ "InCallActivityCommon.setExcludeFromRecents",
+ "RuntimeException when excluding task from recents.",
+ e);
+ }
+ }
+ }
+
+ public void showWifiToLteHandoverToast(DialerCall call) {
+ if (call.hasShownWiFiToLteHandoverToast()) {
+ return;
+ }
+ Toast.makeText(
+ inCallActivity, R.string.video_call_wifi_to_lte_handover_toast, Toast.LENGTH_LONG)
+ .show();
+ call.setHasShownWiFiToLteHandoverToast();
+ }
+
+ public void showWifiFailedDialog(final DialerCall call) {
+ if (call.showWifiHandoverAlertAsToast()) {
+ LogUtil.i("InCallActivityCommon.showWifiFailedDialog", "as toast");
+ Toast.makeText(
+ inCallActivity, R.string.video_call_lte_to_wifi_failed_message, Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+
+ dismissPendingDialogs();
+
+ AlertDialog.Builder builder =
+ new AlertDialog.Builder(inCallActivity)
+ .setTitle(R.string.video_call_lte_to_wifi_failed_title);
+
+ // This allows us to use the theme of the dialog instead of the activity
+ View dialogCheckBoxView =
+ View.inflate(builder.getContext(), R.layout.video_call_lte_to_wifi_failed, null);
+ final CheckBox wifiHandoverFailureCheckbox =
+ (CheckBox) dialogCheckBoxView.findViewById(R.id.video_call_lte_to_wifi_failed_checkbox);
+ wifiHandoverFailureCheckbox.setChecked(false);
+
+ dialog =
+ builder
+ .setView(dialogCheckBoxView)
+ .setMessage(R.string.video_call_lte_to_wifi_failed_message)
+ .setOnCancelListener(
+ new OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ onDialogDismissed();
+ }
+ })
+ .setPositiveButton(
+ android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ call.setDoNotShowDialogForHandoffToWifiFailure(
+ wifiHandoverFailureCheckbox.isChecked());
+ dialog.cancel();
+ onDialogDismissed();
+ }
+ })
+ .create();
+
+ LogUtil.i("InCallActivityCommon.showWifiFailedDialog", "as dialog");
+ dialog.show();
+ }
+
+ public boolean showDialpadFragment(boolean show, boolean animate) {
+ // If the dialpad is already visible, don't animate in. If it's gone, don't animate out.
+ boolean isDialpadVisible = isDialpadVisible();
+ LogUtil.i(
+ "InCallActivityCommon.showDialpadFragment",
+ "show: %b, animate: %b, " + "isDialpadVisible: %b",
+ show,
+ animate,
+ isDialpadVisible);
+ if (show == isDialpadVisible) {
+ return false;
+ }
+
+ FragmentManager dialpadFragmentManager = inCallActivity.getDialpadFragmentManager();
+ if (dialpadFragmentManager == null) {
+ LogUtil.i(
+ "InCallActivityCommon.showDialpadFragment", "unable to show or hide dialpad fragment");
+ return false;
+ }
+
+ // We don't do a FragmentTransaction on the hide case because it will be dealt with when
+ // the listener is fired after an animation finishes.
+ if (!animate) {
+ if (show) {
+ performShowDialpadFragment(dialpadFragmentManager);
+ } else {
+ performHideDialpadFragment();
+ }
+ } else {
+ if (show) {
+ performShowDialpadFragment(dialpadFragmentManager);
+ getDialpadFragment().animateShowDialpad();
+ }
+ getDialpadFragment()
+ .getView()
+ .startAnimation(show ? dialpadSlideInAnimation : dialpadSlideOutAnimation);
+ }
+
+ ProximitySensor sensor = InCallPresenter.getInstance().getProximitySensor();
+ if (sensor != null) {
+ sensor.onDialpadVisible(show);
+ }
+ showDialpadRequest = DIALPAD_REQUEST_NONE;
+ return true;
+ }
+
+ private void performShowDialpadFragment(@NonNull FragmentManager dialpadFragmentManager) {
+ FragmentTransaction transaction = dialpadFragmentManager.beginTransaction();
+ DialpadFragment dialpadFragment = getDialpadFragment();
+ if (dialpadFragment == null) {
+ transaction.add(
+ inCallActivity.getDialpadContainerId(), new DialpadFragment(), TAG_DIALPAD_FRAGMENT);
+ } else {
+ transaction.show(dialpadFragment);
+ }
+
+ transaction.commitAllowingStateLoss();
+ dialpadFragmentManager.executePendingTransactions();
+
+ Logger.get(inCallActivity).logScreenView(ScreenEvent.Type.INCALL_DIALPAD, inCallActivity);
+ }
+
+ private void performHideDialpadFragment() {
+ FragmentManager fragmentManager = inCallActivity.getDialpadFragmentManager();
+ if (fragmentManager == null) {
+ LogUtil.e(
+ "InCallActivityCommon.performHideDialpadFragment", "child fragment manager is null");
+ return;
+ }
+
+ Fragment fragment = fragmentManager.findFragmentByTag(TAG_DIALPAD_FRAGMENT);
+ if (fragment != null) {
+ FragmentTransaction transaction = fragmentManager.beginTransaction();
+ transaction.hide(fragment);
+ transaction.commitAllowingStateLoss();
+ fragmentManager.executePendingTransactions();
+ }
+ }
+
+ public boolean isDialpadVisible() {
+ DialpadFragment dialpadFragment = getDialpadFragment();
+ return dialpadFragment != null && dialpadFragment.isVisible();
+ }
+
+ /** Returns the {@link DialpadFragment} that's shown by this activity, or {@code null} */
+ @Nullable
+ private DialpadFragment getDialpadFragment() {
+ FragmentManager fragmentManager = inCallActivity.getDialpadFragmentManager();
+ if (fragmentManager == null) {
+ return null;
+ }
+ return (DialpadFragment) fragmentManager.findFragmentByTag(TAG_DIALPAD_FRAGMENT);
+ }
+
+ public void updateTaskDescription() {
+ Resources resources = inCallActivity.getResources();
+ int color;
+ if (resources.getBoolean(R.bool.is_layout_landscape)) {
+ color =
+ ResourcesCompat.getColor(
+ resources, R.color.statusbar_background_color, inCallActivity.getTheme());
+ } else {
+ color = InCallPresenter.getInstance().getThemeColorManager().getSecondaryColor();
+ }
+
+ TaskDescription td =
+ new TaskDescription(resources.getString(R.string.notification_ongoing_call), null, color);
+ inCallActivity.setTaskDescription(td);
+ }
+
+ public boolean hasPendingDialogs() {
+ return dialog != null;
+ }
+
+ private void internalResolveIntent(Intent intent) {
+ if (!intent.getAction().equals(Intent.ACTION_MAIN)) {
+ return;
+ }
+
+ if (intent.hasExtra(INTENT_EXTRA_SHOW_DIALPAD)) {
+ // SHOW_DIALPAD_EXTRA can be used here to specify whether the DTMF
+ // dialpad should be initially visible. If the extra isn't
+ // present at all, we just leave the dialpad in its previous state.
+ boolean showDialpad = intent.getBooleanExtra(INTENT_EXTRA_SHOW_DIALPAD, false);
+ LogUtil.i("InCallActivityCommon.internalResolveIntent", "SHOW_DIALPAD_EXTRA: " + showDialpad);
+
+ relaunchedFromDialer(showDialpad);
+ }
+
+ DialerCall outgoingCall = CallList.getInstance().getOutgoingCall();
+ if (outgoingCall == null) {
+ outgoingCall = CallList.getInstance().getPendingOutgoingCall();
+ }
+
+ boolean isNewOutgoingCall = false;
+ if (intent.getBooleanExtra(INTENT_EXTRA_NEW_OUTGOING_CALL, false)) {
+ isNewOutgoingCall = true;
+ intent.removeExtra(INTENT_EXTRA_NEW_OUTGOING_CALL);
+
+ // InCallActivity is responsible for disconnecting a new outgoing call if there
+ // is no way of making it (i.e. no valid call capable accounts).
+ // If the version is not MSIM compatible, then ignore this code.
+ if (CompatUtils.isMSIMCompatible()
+ && InCallPresenter.isCallWithNoValidAccounts(outgoingCall)) {
+ LogUtil.i(
+ "InCallActivityCommon.internalResolveIntent",
+ "call with no valid accounts, disconnecting");
+ outgoingCall.disconnect();
+ }
+
+ dismissKeyguard(true);
+ }
+
+ boolean didShowAccountSelectionDialog = maybeShowAccountSelectionDialog();
+ inCallActivity.onResolveIntent(outgoingCall, isNewOutgoingCall, didShowAccountSelectionDialog);
+ }
+
+ private boolean maybeShowAccountSelectionDialog() {
+ DialerCall call = CallList.getInstance().getWaitingForAccountCall();
+ if (call == null) {
+ return false;
+ }
+
+ Bundle extras = call.getIntentExtras();
+ List<PhoneAccountHandle> phoneAccountHandles;
+ if (extras != null) {
+ phoneAccountHandles =
+ extras.getParcelableArrayList(android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS);
+ } else {
+ phoneAccountHandles = new ArrayList<>();
+ }
+
+ DialogFragment dialogFragment =
+ SelectPhoneAccountDialogFragment.newInstance(
+ R.string.select_phone_account_for_calls,
+ true,
+ phoneAccountHandles,
+ selectAccountListener,
+ call.getId());
+ dialogFragment.show(inCallActivity.getFragmentManager(), TAG_SELECT_ACCOUNT_FRAGMENT);
+ return true;
+ }
+}
diff --git a/java/com/android/incallui/InCallCameraManager.java b/java/com/android/incallui/InCallCameraManager.java
new file mode 100644
index 000000000..fdb422643
--- /dev/null
+++ b/java/com/android/incallui/InCallCameraManager.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui;
+
+import android.content.Context;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/** Used to track which camera is used for outgoing video. */
+public class InCallCameraManager {
+
+ private final Set<Listener> mCameraSelectionListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
+ /** The camera ID for the front facing camera. */
+ private String mFrontFacingCameraId;
+ /** The camera ID for the rear facing camera. */
+ private String mRearFacingCameraId;
+ /** The currently active camera. */
+ private boolean mUseFrontFacingCamera;
+ /**
+ * Indicates whether the list of cameras has been initialized yet. Initialization is delayed until
+ * a video call is present.
+ */
+ private boolean mIsInitialized = false;
+ /** The context. */
+ private Context mContext;
+
+ /**
+ * Initializes the InCall CameraManager.
+ *
+ * @param context The current context.
+ */
+ public InCallCameraManager(Context context) {
+ mUseFrontFacingCamera = true;
+ mContext = context;
+ }
+
+ /**
+ * Sets whether the front facing camera should be used or not.
+ *
+ * @param useFrontFacingCamera {@code True} if the front facing camera is to be used.
+ */
+ public void setUseFrontFacingCamera(boolean useFrontFacingCamera) {
+ mUseFrontFacingCamera = useFrontFacingCamera;
+ for (Listener listener : mCameraSelectionListeners) {
+ listener.onActiveCameraSelectionChanged(mUseFrontFacingCamera);
+ }
+ }
+
+ /**
+ * Determines whether the front facing camera is currently in use.
+ *
+ * @return {@code True} if the front facing camera is in use.
+ */
+ public boolean isUsingFrontFacingCamera() {
+ return mUseFrontFacingCamera;
+ }
+
+ /**
+ * Determines the active camera ID.
+ *
+ * @return The active camera ID.
+ */
+ public String getActiveCameraId() {
+ maybeInitializeCameraList(mContext);
+
+ if (mUseFrontFacingCamera) {
+ return mFrontFacingCameraId;
+ } else {
+ return mRearFacingCameraId;
+ }
+ }
+
+ /** Calls when camera permission is granted by user. */
+ public void onCameraPermissionGranted() {
+ for (Listener listener : mCameraSelectionListeners) {
+ listener.onCameraPermissionGranted();
+ }
+ }
+
+ /**
+ * Get the list of cameras available for use.
+ *
+ * @param context The context.
+ */
+ private void maybeInitializeCameraList(Context context) {
+ if (mIsInitialized || context == null) {
+ return;
+ }
+
+ Log.v(this, "initializeCameraList");
+
+ CameraManager cameraManager = null;
+ try {
+ cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
+ } catch (Exception e) {
+ Log.e(this, "Could not get camera service.");
+ return;
+ }
+
+ if (cameraManager == null) {
+ return;
+ }
+
+ String[] cameraIds = {};
+ try {
+ cameraIds = cameraManager.getCameraIdList();
+ } catch (CameraAccessException e) {
+ Log.d(this, "Could not access camera: " + e);
+ // Camera disabled by device policy.
+ return;
+ }
+
+ for (int i = 0; i < cameraIds.length; i++) {
+ CameraCharacteristics c = null;
+ try {
+ c = cameraManager.getCameraCharacteristics(cameraIds[i]);
+ } catch (IllegalArgumentException e) {
+ // Device Id is unknown.
+ } catch (CameraAccessException e) {
+ // Camera disabled by device policy.
+ }
+ if (c != null) {
+ int facingCharacteristic = c.get(CameraCharacteristics.LENS_FACING);
+ if (facingCharacteristic == CameraCharacteristics.LENS_FACING_FRONT) {
+ mFrontFacingCameraId = cameraIds[i];
+ } else if (facingCharacteristic == CameraCharacteristics.LENS_FACING_BACK) {
+ mRearFacingCameraId = cameraIds[i];
+ }
+ }
+ }
+
+ mIsInitialized = true;
+ Log.v(this, "initializeCameraList : done");
+ }
+
+ public void addCameraSelectionListener(Listener listener) {
+ if (listener != null) {
+ mCameraSelectionListeners.add(listener);
+ }
+ }
+
+ public void removeCameraSelectionListener(Listener listener) {
+ if (listener != null) {
+ mCameraSelectionListeners.remove(listener);
+ }
+ }
+
+ public interface Listener {
+
+ void onActiveCameraSelectionChanged(boolean isUsingFrontFacingCamera);
+
+ void onCameraPermissionGranted();
+ }
+}
diff --git a/java/com/android/incallui/InCallOrientationEventListener.java b/java/com/android/incallui/InCallOrientationEventListener.java
new file mode 100644
index 000000000..e6b0bc027
--- /dev/null
+++ b/java/com/android/incallui/InCallOrientationEventListener.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui;
+
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.support.annotation.IntDef;
+import android.view.OrientationEventListener;
+import com.android.dialer.common.LogUtil;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class listens to Orientation events and overrides onOrientationChanged which gets invoked
+ * when an orientation change occurs. When that happens, we notify InCallUI registrants of the
+ * change.
+ */
+public class InCallOrientationEventListener extends OrientationEventListener {
+
+ public static final int SCREEN_ORIENTATION_0 = 0;
+ public static final int SCREEN_ORIENTATION_90 = 90;
+ public static final int SCREEN_ORIENTATION_180 = 180;
+ public static final int SCREEN_ORIENTATION_270 = 270;
+ public static final int SCREEN_ORIENTATION_360 = 360;
+
+ /** Screen orientation angles one of 0, 90, 180, 270, 360 in degrees. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ SCREEN_ORIENTATION_0,
+ SCREEN_ORIENTATION_90,
+ SCREEN_ORIENTATION_180,
+ SCREEN_ORIENTATION_270,
+ SCREEN_ORIENTATION_360,
+ SCREEN_ORIENTATION_UNKNOWN
+ })
+ public @interface ScreenOrientation {}
+
+ // We use SCREEN_ORIENTATION_USER so that reverse-portrait is not allowed.
+ public static final int ACTIVITY_PREFERENCE_ALLOW_ROTATION = ActivityInfo.SCREEN_ORIENTATION_USER;
+
+ public static final int ACTIVITY_PREFERENCE_DISALLOW_ROTATION =
+ ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+
+ /**
+ * This is to identify dead zones where we won't notify others of orientation changed. Say for e.g
+ * our threshold is x degrees. We will only notify UI when our current rotation is within x
+ * degrees right or left of the screen orientation angles. If it's not within those ranges, we
+ * return SCREEN_ORIENTATION_UNKNOWN and ignore it.
+ */
+ public static final int SCREEN_ORIENTATION_UNKNOWN = -1;
+
+ // Rotation threshold is 10 degrees. So if the rotation angle is within 10 degrees of any of
+ // the above angles, we will notify orientation changed.
+ private static final int ROTATION_THRESHOLD = 10;
+
+ /** Cache the current rotation of the device. */
+ @ScreenOrientation private static int sCurrentOrientation = SCREEN_ORIENTATION_0;
+
+ private boolean mEnabled = false;
+
+ public InCallOrientationEventListener(Context context) {
+ super(context);
+ }
+
+ private static boolean isWithinRange(int value, int begin, int end) {
+ return value >= begin && value < end;
+ }
+
+ private static boolean isWithinThreshold(int value, int center, int threshold) {
+ return isWithinRange(value, center - threshold, center + threshold);
+ }
+
+ private static boolean isInLeftRange(int value, int center, int threshold) {
+ return isWithinRange(value, center - threshold, center);
+ }
+
+ private static boolean isInRightRange(int value, int center, int threshold) {
+ return isWithinRange(value, center, center + threshold);
+ }
+
+ @ScreenOrientation
+ public static int getCurrentOrientation() {
+ return sCurrentOrientation;
+ }
+
+ /**
+ * Handles changes in device orientation. Notifies InCallPresenter of orientation changes.
+ *
+ * <p>Note that this API receives sensor rotation in degrees as a param and we convert that to one
+ * of our screen orientation constants - (one of: {@link #SCREEN_ORIENTATION_0}, {@link
+ * #SCREEN_ORIENTATION_90}, {@link #SCREEN_ORIENTATION_180}, {@link #SCREEN_ORIENTATION_270}).
+ *
+ * @param rotation The new device sensor rotation in degrees
+ */
+ @Override
+ public void onOrientationChanged(int rotation) {
+ if (rotation == OrientationEventListener.ORIENTATION_UNKNOWN) {
+ return;
+ }
+
+ final int orientation = toScreenOrientation(rotation);
+
+ if (orientation != SCREEN_ORIENTATION_UNKNOWN && sCurrentOrientation != orientation) {
+ LogUtil.i(
+ "InCallOrientationEventListener.onOrientationChanged",
+ "orientation: %d -> %d",
+ sCurrentOrientation,
+ orientation);
+ sCurrentOrientation = orientation;
+ InCallPresenter.getInstance().onDeviceOrientationChange(sCurrentOrientation);
+ }
+ }
+
+ /**
+ * Enables the OrientationEventListener and notifies listeners of current orientation if notify
+ * flag is true
+ *
+ * @param notify true or false. Notify device orientation changed if true.
+ */
+ public void enable(boolean notify) {
+ if (mEnabled) {
+ Log.v(this, "enable: Orientation listener is already enabled. Ignoring...");
+ return;
+ }
+
+ super.enable();
+ mEnabled = true;
+ if (notify) {
+ InCallPresenter.getInstance().onDeviceOrientationChange(sCurrentOrientation);
+ }
+ }
+
+ /** Enables the OrientationEventListener with notify flag defaulting to false. */
+ @Override
+ public void enable() {
+ enable(false);
+ }
+
+ /** Disables the OrientationEventListener. */
+ @Override
+ public void disable() {
+ if (!mEnabled) {
+ Log.v(this, "enable: Orientation listener is already disabled. Ignoring...");
+ return;
+ }
+
+ mEnabled = false;
+ super.disable();
+ }
+
+ /** Returns true the OrientationEventListener is enabled, false otherwise. */
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ /**
+ * Converts sensor rotation in degrees to screen orientation constants.
+ *
+ * @param rotation sensor rotation angle in degrees
+ * @return Screen orientation angle in degrees (0, 90, 180, 270). Returns -1 for degrees not
+ * within threshold to identify zones where orientation change should not be trigerred.
+ */
+ @ScreenOrientation
+ private int toScreenOrientation(int rotation) {
+ // Sensor orientation 90 is equivalent to screen orientation 270 and vice versa. This
+ // function returns the screen orientation. So we convert sensor rotation 90 to 270 and
+ // vice versa here.
+ if (isInLeftRange(rotation, SCREEN_ORIENTATION_360, ROTATION_THRESHOLD)
+ || isInRightRange(rotation, SCREEN_ORIENTATION_0, ROTATION_THRESHOLD)) {
+ return SCREEN_ORIENTATION_0;
+ } else if (isWithinThreshold(rotation, SCREEN_ORIENTATION_90, ROTATION_THRESHOLD)) {
+ return SCREEN_ORIENTATION_270;
+ } else if (isWithinThreshold(rotation, SCREEN_ORIENTATION_180, ROTATION_THRESHOLD)) {
+ return SCREEN_ORIENTATION_180;
+ } else if (isWithinThreshold(rotation, SCREEN_ORIENTATION_270, ROTATION_THRESHOLD)) {
+ return SCREEN_ORIENTATION_90;
+ }
+ return SCREEN_ORIENTATION_UNKNOWN;
+ }
+}
diff --git a/java/com/android/incallui/InCallPresenter.java b/java/com/android/incallui/InCallPresenter.java
new file mode 100644
index 000000000..97105fb78
--- /dev/null
+++ b/java/com/android/incallui/InCallPresenter.java
@@ -0,0 +1,1679 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Point;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.telecom.Call.Details;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+import android.view.Window;
+import android.view.WindowManager;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.compat.CallCompat;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler.OnCheckBlockedListener;
+import com.android.dialer.blocking.FilteredNumbersUtil;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.InteractionEvent;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.util.TouchPointManager;
+import com.android.incallui.InCallOrientationEventListener.ScreenOrientation;
+import com.android.incallui.answerproximitysensor.PseudoScreenState;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.call.ExternalCallList;
+import com.android.incallui.call.InCallVideoCallCallbackNotifier;
+import com.android.incallui.call.TelecomAdapter;
+import com.android.incallui.call.VideoUtils;
+import com.android.incallui.latencyreport.LatencyReport;
+import com.android.incallui.legacyblocking.BlockedNumberContentObserver;
+import com.android.incallui.spam.SpamCallListListener;
+import com.android.incallui.util.TelecomCallUtil;
+import com.android.incallui.videosurface.bindings.VideoSurfaceBindings;
+import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Takes updates from the CallList and notifies the InCallActivity (UI) of the changes. Responsible
+ * for starting the activity for a new call and finishing the activity when all calls are
+ * disconnected. Creates and manages the in-call state and provides a listener pattern for the
+ * presenters that want to listen in on the in-call state changes. TODO: This class has become more
+ * of a state machine at this point. Consider renaming.
+ */
+public class InCallPresenter
+ implements CallList.Listener, InCallVideoCallCallbackNotifier.SessionModificationListener {
+
+ private static final String EXTRA_FIRST_TIME_SHOWN =
+ "com.android.incallui.intent.extra.FIRST_TIME_SHOWN";
+
+ private static final long BLOCK_QUERY_TIMEOUT_MS = 1000;
+
+ private static final Bundle EMPTY_EXTRAS = new Bundle();
+
+ private static InCallPresenter sInCallPresenter;
+
+ /**
+ * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is load factor before
+ * resizing, 1 means we only expect a single thread to access the map so make only a single shard
+ */
+ private final Set<InCallStateListener> mListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<InCallStateListener, Boolean>(8, 0.9f, 1));
+
+ private final List<IncomingCallListener> mIncomingCallListeners = new CopyOnWriteArrayList<>();
+ private final Set<InCallDetailsListener> mDetailsListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<InCallDetailsListener, Boolean>(8, 0.9f, 1));
+ private final Set<CanAddCallListener> mCanAddCallListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<CanAddCallListener, Boolean>(8, 0.9f, 1));
+ private final Set<InCallUiListener> mInCallUiListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<InCallUiListener, Boolean>(8, 0.9f, 1));
+ private final Set<InCallOrientationListener> mOrientationListeners =
+ Collections.newSetFromMap(
+ new ConcurrentHashMap<InCallOrientationListener, Boolean>(8, 0.9f, 1));
+ private final Set<InCallEventListener> mInCallEventListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<InCallEventListener, Boolean>(8, 0.9f, 1));
+
+ private StatusBarNotifier mStatusBarNotifier;
+ private ExternalCallNotifier mExternalCallNotifier;
+ private ContactInfoCache mContactInfoCache;
+ private Context mContext;
+ private final OnCheckBlockedListener mOnCheckBlockedListener =
+ new OnCheckBlockedListener() {
+ @Override
+ public void onCheckComplete(final Integer id) {
+ if (id != null && id != FilteredNumberAsyncQueryHandler.INVALID_ID) {
+ // Silence the ringer now to prevent ringing and vibration before the call is
+ // terminated when Telecom attempts to add it.
+ TelecomUtil.silenceRinger(mContext);
+ }
+ }
+ };
+ private CallList mCallList;
+ private ExternalCallList mExternalCallList;
+ private InCallActivity mInCallActivity;
+ private ManageConferenceActivity mManageConferenceActivity;
+ private final android.telecom.Call.Callback mCallCallback =
+ new android.telecom.Call.Callback() {
+ @Override
+ public void onPostDialWait(
+ android.telecom.Call telecomCall, String remainingPostDialSequence) {
+ final DialerCall call = mCallList.getDialerCallFromTelecomCall(telecomCall);
+ if (call == null) {
+ Log.w(this, "DialerCall not found in call list: " + telecomCall);
+ return;
+ }
+ onPostDialCharWait(call.getId(), remainingPostDialSequence);
+ }
+
+ @Override
+ public void onDetailsChanged(
+ android.telecom.Call telecomCall, android.telecom.Call.Details details) {
+ final DialerCall call = mCallList.getDialerCallFromTelecomCall(telecomCall);
+ if (call == null) {
+ Log.w(this, "DialerCall not found in call list: " + telecomCall);
+ return;
+ }
+
+ if (details.hasProperty(Details.PROPERTY_IS_EXTERNAL_CALL)
+ && !mExternalCallList.isCallTracked(telecomCall)) {
+
+ // A regular call became an external call so swap call lists.
+ Log.i(this, "Call became external: " + telecomCall);
+ mCallList.onInternalCallMadeExternal(mContext, telecomCall);
+ mExternalCallList.onCallAdded(telecomCall);
+ return;
+ }
+
+ for (InCallDetailsListener listener : mDetailsListeners) {
+ listener.onDetailsChanged(call, details);
+ }
+ }
+
+ @Override
+ public void onConferenceableCallsChanged(
+ android.telecom.Call telecomCall, List<android.telecom.Call> conferenceableCalls) {
+ Log.i(this, "onConferenceableCallsChanged: " + telecomCall);
+ onDetailsChanged(telecomCall, telecomCall.getDetails());
+ }
+ };
+ private InCallState mInCallState = InCallState.NO_CALLS;
+ private ProximitySensor mProximitySensor;
+ private final PseudoScreenState mPseudoScreenState = new PseudoScreenState();
+ private boolean mServiceConnected;
+ private boolean mAccountSelectionCancelled;
+ private InCallCameraManager mInCallCameraManager;
+ private FilteredNumberAsyncQueryHandler mFilteredQueryHandler;
+ private CallList.Listener mSpamCallListListener;
+ /** Whether or not we are currently bound and waiting for Telecom to send us a new call. */
+ private boolean mBoundAndWaitingForOutgoingCall;
+ /** Determines if the InCall UI is in fullscreen mode or not. */
+ private boolean mIsFullScreen = false;
+
+ private PhoneStateListener mPhoneStateListener =
+ new PhoneStateListener() {
+ @Override
+ public void onCallStateChanged(int state, String incomingNumber) {
+ if (state == TelephonyManager.CALL_STATE_RINGING) {
+ if (FilteredNumbersUtil.hasRecentEmergencyCall(mContext)) {
+ return;
+ }
+ // Check if the number is blocked, to silence the ringer.
+ String countryIso = GeoUtil.getCurrentCountryIso(mContext);
+ mFilteredQueryHandler.isBlockedNumber(
+ mOnCheckBlockedListener, incomingNumber, countryIso);
+ }
+ }
+ };
+ /**
+ * Is true when the activity has been previously started. Some code needs to know not just if the
+ * activity is currently up, but if it had been previously shown in foreground for this in-call
+ * session (e.g., StatusBarNotifier). This gets reset when the session ends in the tear-down
+ * method.
+ */
+ private boolean mIsActivityPreviouslyStarted = false;
+
+ /** Whether or not InCallService is bound to Telecom. */
+ private boolean mServiceBound = false;
+
+ /**
+ * When configuration changes Android kills the current activity and starts a new one. The flag is
+ * used to check if full clean up is necessary (activity is stopped and new activity won't be
+ * started), or if a new activity will be started right after the current one is destroyed, and
+ * therefore no need in release all resources.
+ */
+ private boolean mIsChangingConfigurations = false;
+
+ private boolean mAwaitingCallListUpdate = false;
+
+ private ExternalCallList.ExternalCallListener mExternalCallListener =
+ new ExternalCallList.ExternalCallListener() {
+
+ @Override
+ public void onExternalCallPulled(android.telecom.Call call) {
+ // Note: keep this code in sync with InCallPresenter#onCallAdded
+ LatencyReport latencyReport = new LatencyReport(call);
+ latencyReport.onCallBlockingDone();
+ // Note: External calls do not require spam checking.
+ mCallList.onCallAdded(mContext, call, latencyReport);
+ call.registerCallback(mCallCallback);
+ }
+
+ @Override
+ public void onExternalCallAdded(android.telecom.Call call) {
+ // No-op
+ }
+
+ @Override
+ public void onExternalCallRemoved(android.telecom.Call call) {
+ // No-op
+ }
+
+ @Override
+ public void onExternalCallUpdated(android.telecom.Call call) {
+ // No-op
+ }
+ };
+
+ private ThemeColorManager mThemeColorManager;
+ private VideoSurfaceTexture mLocalVideoSurfaceTexture;
+ private VideoSurfaceTexture mRemoteVideoSurfaceTexture;
+
+ /** Inaccessible constructor. Must use getInstance() to get this singleton. */
+ @VisibleForTesting
+ InCallPresenter() {}
+
+ public static synchronized InCallPresenter getInstance() {
+ if (sInCallPresenter == null) {
+ sInCallPresenter = new InCallPresenter();
+ }
+ return sInCallPresenter;
+ }
+
+ /**
+ * Determines whether or not a call has no valid phone accounts that can be used to make the call
+ * with. Emergency calls do not require a phone account.
+ *
+ * @param call to check accounts for.
+ * @return {@code true} if the call has no call capable phone accounts set, {@code false} if the
+ * call contains a phone account that could be used to initiate it with, or is an emergency
+ * call.
+ */
+ public static boolean isCallWithNoValidAccounts(DialerCall call) {
+ if (call != null && !call.isEmergencyCall()) {
+ Bundle extras = call.getIntentExtras();
+
+ if (extras == null) {
+ extras = EMPTY_EXTRAS;
+ }
+
+ final List<PhoneAccountHandle> phoneAccountHandles =
+ extras.getParcelableArrayList(android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS);
+
+ if ((call.getAccountHandle() == null
+ && (phoneAccountHandles == null || phoneAccountHandles.isEmpty()))) {
+ Log.i(InCallPresenter.getInstance(), "No valid accounts for call " + call);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public InCallState getInCallState() {
+ return mInCallState;
+ }
+
+ public CallList getCallList() {
+ return mCallList;
+ }
+
+ public void setUp(
+ @NonNull Context context,
+ CallList callList,
+ ExternalCallList externalCallList,
+ StatusBarNotifier statusBarNotifier,
+ ExternalCallNotifier externalCallNotifier,
+ ContactInfoCache contactInfoCache,
+ ProximitySensor proximitySensor) {
+ if (mServiceConnected) {
+ Log.i(this, "New service connection replacing existing one.");
+ if (context != mContext || callList != mCallList) {
+ throw new IllegalStateException();
+ }
+ return;
+ }
+
+ Objects.requireNonNull(context);
+ mContext = context;
+
+ mContactInfoCache = contactInfoCache;
+
+ mStatusBarNotifier = statusBarNotifier;
+ mExternalCallNotifier = externalCallNotifier;
+ addListener(mStatusBarNotifier);
+
+ mProximitySensor = proximitySensor;
+ addListener(mProximitySensor);
+
+ mThemeColorManager =
+ new ThemeColorManager(new InCallUIMaterialColorMapUtils(mContext.getResources()));
+
+ mCallList = callList;
+ mExternalCallList = externalCallList;
+ externalCallList.addExternalCallListener(mExternalCallNotifier);
+ externalCallList.addExternalCallListener(mExternalCallListener);
+
+ // This only gets called by the service so this is okay.
+ mServiceConnected = true;
+
+ // The final thing we do in this set up is add ourselves as a listener to CallList. This
+ // will kick off an update and the whole process can start.
+ mCallList.addListener(this);
+
+ // Create spam call list listener and add it to the list of listeners
+ mSpamCallListListener = new SpamCallListListener(context);
+ mCallList.addListener(mSpamCallListListener);
+
+ VideoPauseController.getInstance().setUp(this);
+ InCallVideoCallCallbackNotifier.getInstance().addSessionModificationListener(this);
+
+ mFilteredQueryHandler = new FilteredNumberAsyncQueryHandler(context);
+ mContext
+ .getSystemService(TelephonyManager.class)
+ .listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
+
+ Log.d(this, "Finished InCallPresenter.setUp");
+ }
+
+ /**
+ * Called when the telephony service has disconnected from us. This will happen when there are no
+ * more active calls. However, we may still want to continue showing the UI for certain cases like
+ * showing "Call Ended". What we really want is to wait for the activity and the service to both
+ * disconnect before we tear things down. This method sets a serviceConnected boolean and calls a
+ * secondary method that performs the aforementioned logic.
+ */
+ public void tearDown() {
+ Log.d(this, "tearDown");
+ mCallList.clearOnDisconnect();
+
+ mServiceConnected = false;
+
+ mContext
+ .getSystemService(TelephonyManager.class)
+ .listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
+
+ attemptCleanup();
+ VideoPauseController.getInstance().tearDown();
+ InCallVideoCallCallbackNotifier.getInstance().removeSessionModificationListener(this);
+ }
+
+ private void attemptFinishActivity() {
+ final boolean doFinish = (mInCallActivity != null && isActivityStarted());
+ Log.i(this, "Hide in call UI: " + doFinish);
+ if (doFinish) {
+ mInCallActivity.setExcludeFromRecents(true);
+ mInCallActivity.finish();
+
+ if (mAccountSelectionCancelled) {
+ // This finish is a result of account selection cancellation
+ // do not include activity ending transition
+ mInCallActivity.overridePendingTransition(0, 0);
+ }
+ }
+ }
+
+ /**
+ * Called when the UI ends. Attempts to tear down everything if necessary. See {@link #tearDown()}
+ * for more insight on the tear-down process.
+ */
+ public void unsetActivity(InCallActivity inCallActivity) {
+ if (inCallActivity == null) {
+ throw new IllegalArgumentException("unregisterActivity cannot be called with null");
+ }
+ if (mInCallActivity == null) {
+ Log.i(this, "No InCallActivity currently set, no need to unset.");
+ return;
+ }
+ if (mInCallActivity != inCallActivity) {
+ Log.w(
+ this,
+ "Second instance of InCallActivity is trying to unregister when another"
+ + " instance is active. Ignoring.");
+ return;
+ }
+ updateActivity(null);
+ }
+
+ /**
+ * Updates the current instance of {@link InCallActivity} with the provided one. If a {@code null}
+ * activity is provided, it means that the activity was finished and we should attempt to cleanup.
+ */
+ private void updateActivity(InCallActivity inCallActivity) {
+ boolean updateListeners = false;
+ boolean doAttemptCleanup = false;
+
+ if (inCallActivity != null) {
+ if (mInCallActivity == null) {
+ updateListeners = true;
+ Log.i(this, "UI Initialized");
+ } else {
+ // since setActivity is called onStart(), it can be called multiple times.
+ // This is fine and ignorable, but we do not want to update the world every time
+ // this happens (like going to/from background) so we do not set updateListeners.
+ }
+
+ mInCallActivity = inCallActivity;
+ mInCallActivity.setExcludeFromRecents(false);
+
+ // By the time the UI finally comes up, the call may already be disconnected.
+ // If that's the case, we may need to show an error dialog.
+ if (mCallList != null && mCallList.getDisconnectedCall() != null) {
+ maybeShowErrorDialogOnDisconnect(mCallList.getDisconnectedCall());
+ }
+
+ // When the UI comes up, we need to first check the in-call state.
+ // If we are showing NO_CALLS, that means that a call probably connected and
+ // then immediately disconnected before the UI was able to come up.
+ // If we dont have any calls, start tearing down the UI instead.
+ // NOTE: This code relies on {@link #mInCallActivity} being set so we run it after
+ // it has been set.
+ if (mInCallState == InCallState.NO_CALLS) {
+ Log.i(this, "UI Initialized, but no calls left. shut down.");
+ attemptFinishActivity();
+ return;
+ }
+ } else {
+ Log.i(this, "UI Destroyed");
+ updateListeners = true;
+ mInCallActivity = null;
+
+ // We attempt cleanup for the destroy case but only after we recalculate the state
+ // to see if we need to come back up or stay shut down. This is why we do the
+ // cleanup after the call to onCallListChange() instead of directly here.
+ doAttemptCleanup = true;
+ }
+
+ // Messages can come from the telephony layer while the activity is coming up
+ // and while the activity is going down. So in both cases we need to recalculate what
+ // state we should be in after they complete.
+ // Examples: (1) A new incoming call could come in and then get disconnected before
+ // the activity is created.
+ // (2) All calls could disconnect and then get a new incoming call before the
+ // activity is destroyed.
+ //
+ // b/1122139 - We previously had a check for mServiceConnected here as well, but there are
+ // cases where we need to recalculate the current state even if the service in not
+ // connected. In particular the case where startOrFinish() is called while the app is
+ // already finish()ing. In that case, we skip updating the state with the knowledge that
+ // we will check again once the activity has finished. That means we have to recalculate the
+ // state here even if the service is disconnected since we may not have finished a state
+ // transition while finish()ing.
+ if (updateListeners) {
+ onCallListChange(mCallList);
+ }
+
+ if (doAttemptCleanup) {
+ attemptCleanup();
+ }
+ }
+
+ public void setManageConferenceActivity(
+ @Nullable ManageConferenceActivity manageConferenceActivity) {
+ mManageConferenceActivity = manageConferenceActivity;
+ }
+
+ public void onBringToForeground(boolean showDialpad) {
+ Log.i(this, "Bringing UI to foreground.");
+ bringToForeground(showDialpad);
+ }
+
+ public void onCallAdded(final android.telecom.Call call) {
+ LatencyReport latencyReport = new LatencyReport(call);
+ if (shouldAttemptBlocking(call)) {
+ maybeBlockCall(call, latencyReport);
+ } else {
+ if (call.getDetails().hasProperty(CallCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) {
+ mExternalCallList.onCallAdded(call);
+ } else {
+ latencyReport.onCallBlockingDone();
+ mCallList.onCallAdded(mContext, call, latencyReport);
+ }
+ }
+
+ // Since a call has been added we are no longer waiting for Telecom to send us a call.
+ setBoundAndWaitingForOutgoingCall(false, null);
+ call.registerCallback(mCallCallback);
+ }
+
+ private boolean shouldAttemptBlocking(android.telecom.Call call) {
+ if (call.getState() != android.telecom.Call.STATE_RINGING) {
+ return false;
+ }
+ if (TelecomCallUtil.isEmergencyCall(call)) {
+ Log.i(this, "Not attempting to block incoming emergency call");
+ return false;
+ }
+ if (FilteredNumbersUtil.hasRecentEmergencyCall(mContext)) {
+ Log.i(this, "Not attempting to block incoming call due to recent emergency call");
+ return false;
+ }
+ if (call.getDetails().hasProperty(CallCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Checks whether a call should be blocked, and blocks it if so. Otherwise, it adds the call to
+ * the CallList so it can proceed as normal. There is a timeout, so if the function for checking
+ * whether a function is blocked does not return in a reasonable time, we proceed with adding the
+ * call anyways.
+ */
+ private void maybeBlockCall(final android.telecom.Call call, final LatencyReport latencyReport) {
+ final String countryIso = GeoUtil.getCurrentCountryIso(mContext);
+ final String number = TelecomCallUtil.getNumber(call);
+ final long timeAdded = System.currentTimeMillis();
+
+ // Though AtomicBoolean's can be scary, don't fear, as in this case it is only used on the
+ // main UI thread. It is needed so we can change its value within different scopes, since
+ // that cannot be done with a final boolean.
+ final AtomicBoolean hasTimedOut = new AtomicBoolean(false);
+
+ final Handler handler = new Handler();
+
+ // Proceed if the query is slow; the call may still be blocked after the query returns.
+ final Runnable runnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ hasTimedOut.set(true);
+ latencyReport.onCallBlockingDone();
+ mCallList.onCallAdded(mContext, call, latencyReport);
+ }
+ };
+ handler.postDelayed(runnable, BLOCK_QUERY_TIMEOUT_MS);
+
+ OnCheckBlockedListener onCheckBlockedListener =
+ new OnCheckBlockedListener() {
+ @Override
+ public void onCheckComplete(final Integer id) {
+ if (isReadyForTearDown()) {
+ Log.i(this, "InCallPresenter is torn down, not adding call");
+ return;
+ }
+ if (!hasTimedOut.get()) {
+ handler.removeCallbacks(runnable);
+ }
+ if (id == null) {
+ if (!hasTimedOut.get()) {
+ latencyReport.onCallBlockingDone();
+ mCallList.onCallAdded(mContext, call, latencyReport);
+ }
+ } else if (id == FilteredNumberAsyncQueryHandler.INVALID_ID) {
+ Log.d(this, "checkForBlockedCall: invalid number, skipping block checking");
+ if (!hasTimedOut.get()) {
+ handler.removeCallbacks(runnable);
+
+ latencyReport.onCallBlockingDone();
+ mCallList.onCallAdded(mContext, call, latencyReport);
+ }
+ } else {
+ Log.i(this, "Rejecting incoming call from blocked number");
+ call.reject(false, null);
+ Logger.get(mContext).logInteraction(InteractionEvent.Type.CALL_BLOCKED);
+
+ /*
+ * If mContext is null, then the InCallPresenter was torn down before the
+ * block check had a chance to complete. The context is no longer valid, so
+ * don't attempt to remove the call log entry.
+ */
+ if (mContext == null) {
+ return;
+ }
+ // Register observer to update the call log.
+ // BlockedNumberContentObserver will unregister after successful log or timeout.
+ BlockedNumberContentObserver contentObserver =
+ new BlockedNumberContentObserver(mContext, new Handler(), number, timeAdded);
+ contentObserver.register();
+ }
+ }
+ };
+
+ mFilteredQueryHandler.isBlockedNumber(onCheckBlockedListener, number, countryIso);
+ }
+
+ public void onCallRemoved(android.telecom.Call call) {
+ if (call.getDetails().hasProperty(CallCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) {
+ mExternalCallList.onCallRemoved(call);
+ } else {
+ mCallList.onCallRemoved(mContext, call);
+ call.unregisterCallback(mCallCallback);
+ }
+ }
+
+ public void onCanAddCallChanged(boolean canAddCall) {
+ for (CanAddCallListener listener : mCanAddCallListeners) {
+ listener.onCanAddCallChanged(canAddCall);
+ }
+ }
+
+ @Override
+ public void onWiFiToLteHandover(DialerCall call) {
+ if (mInCallActivity != null) {
+ mInCallActivity.onWiFiToLteHandover(call);
+ }
+ }
+
+ @Override
+ public void onHandoverToWifiFailed(DialerCall call) {
+ if (mInCallActivity != null) {
+ mInCallActivity.onHandoverToWifiFailed(call);
+ }
+ }
+
+ /**
+ * Called when there is a change to the call list. Sets the In-Call state for the entire in-call
+ * app based on the information it gets from CallList. Dispatches the in-call state to all
+ * listeners. Can trigger the creation or destruction of the UI based on the states that is
+ * calculates.
+ */
+ @Override
+ public void onCallListChange(CallList callList) {
+ if (mInCallActivity != null && mInCallActivity.isInCallScreenAnimating()) {
+ mAwaitingCallListUpdate = true;
+ return;
+ }
+ if (callList == null) {
+ return;
+ }
+
+ mAwaitingCallListUpdate = false;
+
+ InCallState newState = getPotentialStateFromCallList(callList);
+ InCallState oldState = mInCallState;
+ Log.d(this, "onCallListChange oldState= " + oldState + " newState=" + newState);
+ newState = startOrFinishUi(newState);
+ Log.d(this, "onCallListChange newState changed to " + newState);
+
+ // Set the new state before announcing it to the world
+ Log.i(this, "Phone switching state: " + oldState + " -> " + newState);
+ mInCallState = newState;
+
+ // notify listeners of new state
+ for (InCallStateListener listener : mListeners) {
+ Log.d(this, "Notify " + listener + " of state " + mInCallState.toString());
+ listener.onStateChange(oldState, mInCallState, callList);
+ }
+
+ if (isActivityStarted()) {
+ final boolean hasCall =
+ callList.getActiveOrBackgroundCall() != null || callList.getOutgoingCall() != null;
+ mInCallActivity.dismissKeyguard(hasCall);
+ }
+ }
+
+ /** Called when there is a new incoming call. */
+ @Override
+ public void onIncomingCall(DialerCall call) {
+ InCallState newState = startOrFinishUi(InCallState.INCOMING);
+ InCallState oldState = mInCallState;
+
+ Log.i(this, "Phone switching state: " + oldState + " -> " + newState);
+ mInCallState = newState;
+
+ for (IncomingCallListener listener : mIncomingCallListeners) {
+ listener.onIncomingCall(oldState, mInCallState, call);
+ }
+
+ if (mInCallActivity != null) {
+ // Re-evaluate which fragment is being shown.
+ mInCallActivity.onPrimaryCallStateChanged();
+ }
+ }
+
+ @Override
+ public void onUpgradeToVideo(DialerCall call) {
+ if (call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST
+ && mInCallState == InCallPresenter.InCallState.INCOMING) {
+ LogUtil.i(
+ "InCallPresenter.onUpgradeToVideo",
+ "rejecting upgrade request due to existing incoming call");
+ call.declineUpgradeRequest();
+ }
+
+ if (mInCallActivity != null) {
+ // Re-evaluate which fragment is being shown.
+ mInCallActivity.onPrimaryCallStateChanged();
+ }
+ }
+
+ @Override
+ public void onSessionModificationStateChange(@SessionModificationState int newState) {
+ LogUtil.i("InCallPresenter.onSessionModificationStateChange", "state: %d", newState);
+ if (mProximitySensor == null) {
+ LogUtil.i("InCallPresenter.onSessionModificationStateChange", "proximitySensor is null");
+ return;
+ }
+ mProximitySensor.setIsAttemptingVideoCall(
+ VideoUtils.hasSentVideoUpgradeRequest(newState)
+ || VideoUtils.hasReceivedVideoUpgradeRequest(newState));
+ if (mInCallActivity != null) {
+ // Re-evaluate which fragment is being shown.
+ mInCallActivity.onPrimaryCallStateChanged();
+ }
+ }
+
+ /**
+ * Called when a call becomes disconnected. Called everytime an existing call changes from being
+ * connected (incoming/outgoing/active) to disconnected.
+ */
+ @Override
+ public void onDisconnect(DialerCall call) {
+ maybeShowErrorDialogOnDisconnect(call);
+
+ // We need to do the run the same code as onCallListChange.
+ onCallListChange(mCallList);
+
+ if (isActivityStarted()) {
+ mInCallActivity.dismissKeyguard(false);
+ }
+
+ if (call.isEmergencyCall()) {
+ FilteredNumbersUtil.recordLastEmergencyCallTime(mContext);
+ }
+ }
+
+ @Override
+ public void onUpgradeToVideoRequest(DialerCall call, int videoState) {
+ LogUtil.d(
+ "InCallPresenter.onUpgradeToVideoRequest",
+ "call = " + call + " video state = " + videoState);
+
+ if (call == null) {
+ return;
+ }
+
+ call.setRequestedVideoState(videoState);
+ }
+
+ /** Given the call list, return the state in which the in-call screen should be. */
+ public InCallState getPotentialStateFromCallList(CallList callList) {
+
+ InCallState newState = InCallState.NO_CALLS;
+
+ if (callList == null) {
+ return newState;
+ }
+ if (callList.getIncomingCall() != null) {
+ newState = InCallState.INCOMING;
+ } else if (callList.getWaitingForAccountCall() != null) {
+ newState = InCallState.WAITING_FOR_ACCOUNT;
+ } else if (callList.getPendingOutgoingCall() != null) {
+ newState = InCallState.PENDING_OUTGOING;
+ } else if (callList.getOutgoingCall() != null) {
+ newState = InCallState.OUTGOING;
+ } else if (callList.getActiveCall() != null
+ || callList.getBackgroundCall() != null
+ || callList.getDisconnectedCall() != null
+ || callList.getDisconnectingCall() != null) {
+ newState = InCallState.INCALL;
+ }
+
+ if (newState == InCallState.NO_CALLS) {
+ if (mBoundAndWaitingForOutgoingCall) {
+ return InCallState.OUTGOING;
+ }
+ }
+
+ return newState;
+ }
+
+ public boolean isBoundAndWaitingForOutgoingCall() {
+ return mBoundAndWaitingForOutgoingCall;
+ }
+
+ public void setBoundAndWaitingForOutgoingCall(boolean isBound, PhoneAccountHandle handle) {
+ Log.i(this, "setBoundAndWaitingForOutgoingCall: " + isBound);
+ mBoundAndWaitingForOutgoingCall = isBound;
+ mThemeColorManager.setPendingPhoneAccountHandle(handle);
+ if (isBound && mInCallState == InCallState.NO_CALLS) {
+ mInCallState = InCallState.OUTGOING;
+ }
+ }
+
+ public void onShrinkAnimationComplete() {
+ if (mAwaitingCallListUpdate) {
+ onCallListChange(mCallList);
+ }
+ }
+
+ public void addIncomingCallListener(IncomingCallListener listener) {
+ Objects.requireNonNull(listener);
+ mIncomingCallListeners.add(listener);
+ }
+
+ public void removeIncomingCallListener(IncomingCallListener listener) {
+ if (listener != null) {
+ mIncomingCallListeners.remove(listener);
+ }
+ }
+
+ public void addListener(InCallStateListener listener) {
+ Objects.requireNonNull(listener);
+ mListeners.add(listener);
+ }
+
+ public void removeListener(InCallStateListener listener) {
+ if (listener != null) {
+ mListeners.remove(listener);
+ }
+ }
+
+ public void addDetailsListener(InCallDetailsListener listener) {
+ Objects.requireNonNull(listener);
+ mDetailsListeners.add(listener);
+ }
+
+ public void removeDetailsListener(InCallDetailsListener listener) {
+ if (listener != null) {
+ mDetailsListeners.remove(listener);
+ }
+ }
+
+ public void addCanAddCallListener(CanAddCallListener listener) {
+ Objects.requireNonNull(listener);
+ mCanAddCallListeners.add(listener);
+ }
+
+ public void removeCanAddCallListener(CanAddCallListener listener) {
+ if (listener != null) {
+ mCanAddCallListeners.remove(listener);
+ }
+ }
+
+ public void addOrientationListener(InCallOrientationListener listener) {
+ Objects.requireNonNull(listener);
+ mOrientationListeners.add(listener);
+ }
+
+ public void removeOrientationListener(InCallOrientationListener listener) {
+ if (listener != null) {
+ mOrientationListeners.remove(listener);
+ }
+ }
+
+ public void addInCallEventListener(InCallEventListener listener) {
+ Objects.requireNonNull(listener);
+ mInCallEventListeners.add(listener);
+ }
+
+ public void removeInCallEventListener(InCallEventListener listener) {
+ if (listener != null) {
+ mInCallEventListeners.remove(listener);
+ }
+ }
+
+ public ProximitySensor getProximitySensor() {
+ return mProximitySensor;
+ }
+
+ public PseudoScreenState getPseudoScreenState() {
+ return mPseudoScreenState;
+ }
+
+ /** Returns true if the incall app is the foreground application. */
+ public boolean isShowingInCallUi() {
+ if (!isActivityStarted()) {
+ return false;
+ }
+ if (mManageConferenceActivity != null && mManageConferenceActivity.isVisible()) {
+ return true;
+ }
+ return mInCallActivity.isVisible();
+ }
+
+ /**
+ * Returns true if the activity has been created and is running. Returns true as long as activity
+ * is not destroyed or finishing. This ensures that we return true even if the activity is paused
+ * (not in foreground).
+ */
+ public boolean isActivityStarted() {
+ return (mInCallActivity != null
+ && !mInCallActivity.isDestroyed()
+ && !mInCallActivity.isFinishing());
+ }
+
+ /**
+ * Determines if the In-Call app is currently changing configuration.
+ *
+ * @return {@code true} if the In-Call app is changing configuration.
+ */
+ public boolean isChangingConfigurations() {
+ return mIsChangingConfigurations;
+ }
+
+ /**
+ * Tracks whether the In-Call app is currently in the process of changing configuration (i.e.
+ * screen orientation).
+ */
+ /*package*/
+ void updateIsChangingConfigurations() {
+ mIsChangingConfigurations = false;
+ if (mInCallActivity != null) {
+ mIsChangingConfigurations = mInCallActivity.isChangingConfigurations();
+ }
+ Log.v(this, "updateIsChangingConfigurations = " + mIsChangingConfigurations);
+ }
+
+ /** Called when the activity goes in/out of the foreground. */
+ public void onUiShowing(boolean showing) {
+ // We need to update the notification bar when we leave the UI because that
+ // could trigger it to show again.
+ if (mStatusBarNotifier != null) {
+ mStatusBarNotifier.updateNotification(mCallList);
+ }
+
+ if (mProximitySensor != null) {
+ mProximitySensor.onInCallShowing(showing);
+ }
+
+ Intent broadcastIntent = Bindings.get(mContext).getUiReadyBroadcastIntent(mContext);
+ if (broadcastIntent != null) {
+ broadcastIntent.putExtra(EXTRA_FIRST_TIME_SHOWN, !mIsActivityPreviouslyStarted);
+
+ if (showing) {
+ Log.d(this, "Sending sticky broadcast: ", broadcastIntent);
+ mContext.sendStickyBroadcast(broadcastIntent);
+ } else {
+ Log.d(this, "Removing sticky broadcast: ", broadcastIntent);
+ mContext.removeStickyBroadcast(broadcastIntent);
+ }
+ }
+
+ if (showing) {
+ mIsActivityPreviouslyStarted = true;
+ } else {
+ updateIsChangingConfigurations();
+ }
+
+ for (InCallUiListener listener : mInCallUiListeners) {
+ listener.onUiShowing(showing);
+ }
+
+ if (mInCallActivity != null) {
+ // Re-evaluate which fragment is being shown.
+ mInCallActivity.onPrimaryCallStateChanged();
+ }
+ }
+
+ public void addInCallUiListener(InCallUiListener listener) {
+ mInCallUiListeners.add(listener);
+ }
+
+ public boolean removeInCallUiListener(InCallUiListener listener) {
+ return mInCallUiListeners.remove(listener);
+ }
+
+ /*package*/
+ void onActivityStarted() {
+ Log.d(this, "onActivityStarted");
+ notifyVideoPauseController(true);
+ mStatusBarNotifier.updateNotification(mCallList);
+ }
+
+ /*package*/
+ void onActivityStopped() {
+ Log.d(this, "onActivityStopped");
+ notifyVideoPauseController(false);
+ }
+
+ private void notifyVideoPauseController(boolean showing) {
+ Log.d(
+ this, "notifyVideoPauseController: mIsChangingConfigurations=" + mIsChangingConfigurations);
+ if (!mIsChangingConfigurations) {
+ VideoPauseController.getInstance().onUiShowing(showing);
+ }
+ }
+
+ /** Brings the app into the foreground if possible. */
+ public void bringToForeground(boolean showDialpad) {
+ // Before we bring the incall UI to the foreground, we check to see if:
+ // 1. It is not currently in the foreground
+ // 2. We are in a state where we want to show the incall ui (i.e. there are calls to
+ // be displayed)
+ // If the activity hadn't actually been started previously, yet there are still calls
+ // present (e.g. a call was accepted by a bluetooth or wired headset), we want to
+ // bring it up the UI regardless.
+ if (!isShowingInCallUi() && mInCallState != InCallState.NO_CALLS) {
+ showInCall(showDialpad, false /* newOutgoingCall */, false /* isVideoCall */);
+ }
+ }
+
+ public void onPostDialCharWait(String callId, String chars) {
+ if (isActivityStarted()) {
+ mInCallActivity.showPostCharWaitDialog(callId, chars);
+ }
+ }
+
+ /**
+ * Handles the green CALL key while in-call.
+ *
+ * @return true if we consumed the event.
+ */
+ public boolean handleCallKey() {
+ LogUtil.v("InCallPresenter.handleCallKey", null);
+
+ // The green CALL button means either "Answer", "Unhold", or
+ // "Swap calls", or can be a no-op, depending on the current state
+ // of the Phone.
+
+ /** INCOMING CALL */
+ final CallList calls = mCallList;
+ final DialerCall incomingCall = calls.getIncomingCall();
+ LogUtil.v("InCallPresenter.handleCallKey", "incomingCall: " + incomingCall);
+
+ // (1) Attempt to answer a call
+ if (incomingCall != null) {
+ incomingCall.answer(VideoProfile.STATE_AUDIO_ONLY);
+ return true;
+ }
+
+ /** STATE_ACTIVE CALL */
+ final DialerCall activeCall = calls.getActiveCall();
+ if (activeCall != null) {
+ // TODO: This logic is repeated from CallButtonPresenter.java. We should
+ // consolidate this logic.
+ final boolean canMerge =
+ activeCall.can(android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE);
+ final boolean canSwap =
+ activeCall.can(android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE);
+
+ Log.v(
+ this, "activeCall: " + activeCall + ", canMerge: " + canMerge + ", canSwap: " + canSwap);
+
+ // (2) Attempt actions on conference calls
+ if (canMerge) {
+ TelecomAdapter.getInstance().merge(activeCall.getId());
+ return true;
+ } else if (canSwap) {
+ TelecomAdapter.getInstance().swap(activeCall.getId());
+ return true;
+ }
+ }
+
+ /** BACKGROUND CALL */
+ final DialerCall heldCall = calls.getBackgroundCall();
+ if (heldCall != null) {
+ // We have a hold call so presumeable it will always support HOLD...but
+ // there is no harm in double checking.
+ final boolean canHold = heldCall.can(android.telecom.Call.Details.CAPABILITY_HOLD);
+
+ Log.v(this, "heldCall: " + heldCall + ", canHold: " + canHold);
+
+ // (4) unhold call
+ if (heldCall.getState() == DialerCall.State.ONHOLD && canHold) {
+ heldCall.unhold();
+ return true;
+ }
+ }
+
+ // Always consume hard keys
+ return true;
+ }
+
+ /**
+ * A dialog could have prevented in-call screen from being previously finished. This function
+ * checks to see if there should be any UI left and if not attempts to tear down the UI.
+ */
+ public void onDismissDialog() {
+ Log.i(this, "Dialog dismissed");
+ if (mInCallState == InCallState.NO_CALLS) {
+ attemptFinishActivity();
+ attemptCleanup();
+ }
+ }
+
+ /** Clears the previous fullscreen state. */
+ public void clearFullscreen() {
+ mIsFullScreen = false;
+ }
+
+ /**
+ * Changes the fullscreen mode of the in-call UI.
+ *
+ * @param isFullScreen {@code true} if in-call should be in fullscreen mode, {@code false}
+ * otherwise.
+ */
+ public void setFullScreen(boolean isFullScreen) {
+ setFullScreen(isFullScreen, false /* force */);
+ }
+
+ /**
+ * Changes the fullscreen mode of the in-call UI.
+ *
+ * @param isFullScreen {@code true} if in-call should be in fullscreen mode, {@code false}
+ * otherwise.
+ * @param force {@code true} if fullscreen mode should be set regardless of its current state.
+ */
+ public void setFullScreen(boolean isFullScreen, boolean force) {
+ Log.i(this, "setFullScreen = " + isFullScreen);
+
+ // As a safeguard, ensure we cannot enter fullscreen if the dialpad is shown.
+ if (isDialpadVisible()) {
+ isFullScreen = false;
+ Log.v(this, "setFullScreen overridden as dialpad is shown = " + isFullScreen);
+ }
+
+ if (mIsFullScreen == isFullScreen && !force) {
+ Log.v(this, "setFullScreen ignored as already in that state.");
+ return;
+ }
+ mIsFullScreen = isFullScreen;
+ notifyFullscreenModeChange(mIsFullScreen);
+ }
+
+ /**
+ * @return {@code true} if the in-call ui is currently in fullscreen mode, {@code false}
+ * otherwise.
+ */
+ public boolean isFullscreen() {
+ return mIsFullScreen;
+ }
+
+ /**
+ * Called by the {@link VideoCallPresenter} to inform of a change in full screen video status.
+ *
+ * @param isFullscreenMode {@code True} if entering full screen mode.
+ */
+ public void notifyFullscreenModeChange(boolean isFullscreenMode) {
+ for (InCallEventListener listener : mInCallEventListeners) {
+ listener.onFullscreenModeChanged(isFullscreenMode);
+ }
+ }
+
+ /**
+ * For some disconnected causes, we show a dialog. This calls into the activity to show the dialog
+ * if appropriate for the call.
+ */
+ private void maybeShowErrorDialogOnDisconnect(DialerCall call) {
+ // For newly disconnected calls, we may want to show a dialog on specific error conditions
+ if (isActivityStarted() && call.getState() == DialerCall.State.DISCONNECTED) {
+ if (call.getAccountHandle() == null && !call.isConferenceCall()) {
+ setDisconnectCauseForMissingAccounts(call);
+ }
+ mInCallActivity.maybeShowErrorDialogOnDisconnect(call.getDisconnectCause());
+ }
+ }
+
+ /**
+ * When the state of in-call changes, this is the first method to get called. It determines if the
+ * UI needs to be started or finished depending on the new state and does it.
+ */
+ private InCallState startOrFinishUi(InCallState newState) {
+ Log.d(this, "startOrFinishUi: " + mInCallState + " -> " + newState);
+
+ // TODO: Consider a proper state machine implementation
+
+ // If the state isn't changing we have already done any starting/stopping of activities in
+ // a previous pass...so lets cut out early
+ if (newState == mInCallState) {
+ return newState;
+ }
+
+ // A new Incoming call means that the user needs to be notified of the the call (since
+ // it wasn't them who initiated it). We do this through full screen notifications and
+ // happens indirectly through {@link StatusBarNotifier}.
+ //
+ // The process for incoming calls is as follows:
+ //
+ // 1) CallList - Announces existence of new INCOMING call
+ // 2) InCallPresenter - Gets announcement and calculates that the new InCallState
+ // - should be set to INCOMING.
+ // 3) InCallPresenter - This method is called to see if we need to start or finish
+ // the app given the new state.
+ // 4) StatusBarNotifier - Listens to InCallState changes. InCallPresenter calls
+ // StatusBarNotifier explicitly to issue a FullScreen Notification
+ // that will either start the InCallActivity or show the user a
+ // top-level notification dialog if the user is in an immersive app.
+ // That notification can also start the InCallActivity.
+ // 5) InCallActivity - Main activity starts up and at the end of its onCreate will
+ // call InCallPresenter::setActivity() to let the presenter
+ // know that start-up is complete.
+ //
+ // [ AND NOW YOU'RE IN THE CALL. voila! ]
+ //
+ // Our app is started using a fullScreen notification. We need to do this whenever
+ // we get an incoming call. Depending on the current context of the device, either a
+ // incoming call HUN or the actual InCallActivity will be shown.
+ final boolean startIncomingCallSequence = (InCallState.INCOMING == newState);
+
+ // A dialog to show on top of the InCallUI to select a PhoneAccount
+ final boolean showAccountPicker = (InCallState.WAITING_FOR_ACCOUNT == newState);
+
+ // A new outgoing call indicates that the user just now dialed a number and when that
+ // happens we need to display the screen immediately or show an account picker dialog if
+ // no default is set. However, if the main InCallUI is already visible, we do not want to
+ // re-initiate the start-up animation, so we do not need to do anything here.
+ //
+ // It is also possible to go into an intermediate state where the call has been initiated
+ // but Telecom has not yet returned with the details of the call (handle, gateway, etc.).
+ // This pending outgoing state can also launch the call screen.
+ //
+ // This is different from the incoming call sequence because we do not need to shock the
+ // user with a top-level notification. Just show the call UI normally.
+ boolean callCardFragmentVisible =
+ mInCallActivity != null && mInCallActivity.getCallCardFragmentVisible();
+ final boolean mainUiNotVisible = !isShowingInCallUi() || !callCardFragmentVisible;
+ boolean showCallUi = InCallState.OUTGOING == newState && mainUiNotVisible;
+
+ // Direct transition from PENDING_OUTGOING -> INCALL means that there was an error in the
+ // outgoing call process, so the UI should be brought up to show an error dialog.
+ showCallUi |=
+ (InCallState.PENDING_OUTGOING == mInCallState
+ && InCallState.INCALL == newState
+ && !isShowingInCallUi());
+
+ // Another exception - InCallActivity is in charge of disconnecting a call with no
+ // valid accounts set. Bring the UI up if this is true for the current pending outgoing
+ // call so that:
+ // 1) The call can be disconnected correctly
+ // 2) The UI comes up and correctly displays the error dialog.
+ // TODO: Remove these special case conditions by making InCallPresenter a true state
+ // machine. Telecom should also be the component responsible for disconnecting a call
+ // with no valid accounts.
+ showCallUi |=
+ InCallState.PENDING_OUTGOING == newState
+ && mainUiNotVisible
+ && isCallWithNoValidAccounts(mCallList.getPendingOutgoingCall());
+
+ // The only time that we have an instance of mInCallActivity and it isn't started is
+ // when it is being destroyed. In that case, lets avoid bringing up another instance of
+ // the activity. When it is finally destroyed, we double check if we should bring it back
+ // up so we aren't going to lose anything by avoiding a second startup here.
+ boolean activityIsFinishing = mInCallActivity != null && !isActivityStarted();
+ if (activityIsFinishing) {
+ Log.i(this, "Undo the state change: " + newState + " -> " + mInCallState);
+ return mInCallState;
+ }
+
+ // We're about the bring up the in-call UI for outgoing and incoming call. If we still have
+ // dialogs up, we need to clear them out before showing in-call screen. This is necessary
+ // to fix the bug that dialog will show up when data reaches limit even after makeing new
+ // outgoing call after user ignore it by pressing home button.
+ if ((newState == InCallState.INCOMING || newState == InCallState.PENDING_OUTGOING)
+ && !showCallUi
+ && isActivityStarted()) {
+ mInCallActivity.dismissPendingDialogs();
+ }
+
+ if (showCallUi || showAccountPicker) {
+ Log.i(this, "Start in call UI");
+ showInCall(false /* showDialpad */, !showAccountPicker /* newOutgoingCall */, false);
+ } else if (startIncomingCallSequence) {
+ Log.i(this, "Start Full Screen in call UI");
+
+ if (!startUi()) {
+ // startUI refused to start the UI. This indicates that it needed to restart the
+ // activity. When it finally restarts, it will call us back, so we do not actually
+ // change the state yet (we return mInCallState instead of newState).
+ return mInCallState;
+ }
+ } else if (newState == InCallState.NO_CALLS) {
+ // The new state is the no calls state. Tear everything down.
+ attemptFinishActivity();
+ attemptCleanup();
+ }
+
+ return newState;
+ }
+
+ /**
+ * Sets the DisconnectCause for a call that was disconnected because it was missing a PhoneAccount
+ * or PhoneAccounts to select from.
+ */
+ private void setDisconnectCauseForMissingAccounts(DialerCall call) {
+
+ Bundle extras = call.getIntentExtras();
+ // Initialize the extras bundle to avoid NPE
+ if (extras == null) {
+ extras = new Bundle();
+ }
+
+ final List<PhoneAccountHandle> phoneAccountHandles =
+ extras.getParcelableArrayList(android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS);
+
+ if (phoneAccountHandles == null || phoneAccountHandles.isEmpty()) {
+ String scheme = call.getHandle().getScheme();
+ final String errorMsg =
+ PhoneAccount.SCHEME_TEL.equals(scheme)
+ ? mContext.getString(R.string.callFailed_simError)
+ : mContext.getString(R.string.incall_error_supp_service_unknown);
+ DisconnectCause disconnectCause =
+ new DisconnectCause(DisconnectCause.ERROR, null, errorMsg, errorMsg);
+ call.setDisconnectCause(disconnectCause);
+ }
+ }
+
+ private boolean startUi() {
+ boolean isCallWaiting =
+ mCallList.getActiveCall() != null && mCallList.getIncomingCall() != null;
+
+ if (isCallWaiting) {
+ showInCall(false, false, false /* isVideoCall */);
+ } else {
+ mStatusBarNotifier.updateNotification(mCallList);
+ }
+ return true;
+ }
+
+ /**
+ * @return {@code true} if the InCallPresenter is ready to be torn down, {@code false} otherwise.
+ * Calling classes should use this as an indication whether to interact with the
+ * InCallPresenter or not.
+ */
+ public boolean isReadyForTearDown() {
+ return mInCallActivity == null && !mServiceConnected && mInCallState == InCallState.NO_CALLS;
+ }
+
+ /**
+ * Checks to see if both the UI is gone and the service is disconnected. If so, tear it all down.
+ */
+ private void attemptCleanup() {
+ if (isReadyForTearDown()) {
+ Log.i(this, "Cleaning up");
+
+ cleanupSurfaces();
+
+ mIsActivityPreviouslyStarted = false;
+ mIsChangingConfigurations = false;
+
+ // blow away stale contact info so that we get fresh data on
+ // the next set of calls
+ if (mContactInfoCache != null) {
+ mContactInfoCache.clearCache();
+ }
+ mContactInfoCache = null;
+
+ if (mProximitySensor != null) {
+ removeListener(mProximitySensor);
+ mProximitySensor.tearDown();
+ }
+ mProximitySensor = null;
+
+ if (mStatusBarNotifier != null) {
+ removeListener(mStatusBarNotifier);
+ }
+ if (mExternalCallNotifier != null && mExternalCallList != null) {
+ mExternalCallList.removeExternalCallListener(mExternalCallNotifier);
+ }
+ mStatusBarNotifier = null;
+
+ if (mCallList != null) {
+ mCallList.removeListener(this);
+ mCallList.removeListener(mSpamCallListListener);
+ }
+ mCallList = null;
+
+ mContext = null;
+ mInCallActivity = null;
+ mManageConferenceActivity = null;
+
+ mListeners.clear();
+ mIncomingCallListeners.clear();
+ mDetailsListeners.clear();
+ mCanAddCallListeners.clear();
+ mOrientationListeners.clear();
+ mInCallEventListeners.clear();
+ mInCallUiListeners.clear();
+
+ Log.d(this, "Finished InCallPresenter.CleanUp");
+ }
+ }
+
+ public void showInCall(boolean showDialpad, boolean newOutgoingCall, boolean isVideoCall) {
+ Log.i(this, "Showing InCallActivity");
+ mContext.startActivity(
+ InCallActivity.getIntent(
+ mContext, showDialpad, newOutgoingCall, isVideoCall, false /* forFullScreen */));
+ }
+
+ public void onServiceBind() {
+ mServiceBound = true;
+ }
+
+ public void onServiceUnbind() {
+ InCallPresenter.getInstance().setBoundAndWaitingForOutgoingCall(false, null);
+ mServiceBound = false;
+ }
+
+ public boolean isServiceBound() {
+ return mServiceBound;
+ }
+
+ public void maybeStartRevealAnimation(Intent intent) {
+ if (intent == null || mInCallActivity != null) {
+ return;
+ }
+ final Bundle extras = intent.getBundleExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS);
+ if (extras == null) {
+ // Incoming call, just show the in-call UI directly.
+ return;
+ }
+
+ if (extras.containsKey(android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS)) {
+ // Account selection dialog will show up so don't show the animation.
+ return;
+ }
+
+ final PhoneAccountHandle accountHandle =
+ intent.getParcelableExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
+ final Point touchPoint = extras.getParcelable(TouchPointManager.TOUCH_POINT);
+ int videoState =
+ extras.getInt(
+ TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, VideoProfile.STATE_AUDIO_ONLY);
+
+ InCallPresenter.getInstance().setBoundAndWaitingForOutgoingCall(true, accountHandle);
+
+ final Intent activityIntent =
+ InCallActivity.getIntent(
+ mContext, false, true, VideoUtils.isVideoCall(videoState), false /* forFullScreen */);
+ activityIntent.putExtra(TouchPointManager.TOUCH_POINT, touchPoint);
+ mContext.startActivity(activityIntent);
+ }
+
+ /**
+ * Retrieves the current in-call camera manager instance, creating if necessary.
+ *
+ * @return The {@link InCallCameraManager}.
+ */
+ public InCallCameraManager getInCallCameraManager() {
+ synchronized (this) {
+ if (mInCallCameraManager == null) {
+ mInCallCameraManager = new InCallCameraManager(mContext);
+ }
+
+ return mInCallCameraManager;
+ }
+ }
+
+ /**
+ * Notifies listeners of changes in orientation and notify calls of rotation angle change.
+ *
+ * @param orientation The screen orientation of the device (one of: {@link
+ * InCallOrientationEventListener#SCREEN_ORIENTATION_0}, {@link
+ * InCallOrientationEventListener#SCREEN_ORIENTATION_90}, {@link
+ * InCallOrientationEventListener#SCREEN_ORIENTATION_180}, {@link
+ * InCallOrientationEventListener#SCREEN_ORIENTATION_270}).
+ */
+ public void onDeviceOrientationChange(@ScreenOrientation int orientation) {
+ Log.d(this, "onDeviceOrientationChange: orientation= " + orientation);
+
+ if (mCallList != null) {
+ mCallList.notifyCallsOfDeviceRotation(orientation);
+ } else {
+ Log.w(this, "onDeviceOrientationChange: CallList is null.");
+ }
+
+ // Notify listeners of device orientation changed.
+ for (InCallOrientationListener listener : mOrientationListeners) {
+ listener.onDeviceOrientationChanged(orientation);
+ }
+ }
+
+ /**
+ * Configures the in-call UI activity so it can change orientations or not. Enables the
+ * orientation event listener if allowOrientationChange is true, disables it if false.
+ *
+ * @param allowOrientationChange {@code true} if the in-call UI can change between portrait and
+ * landscape. {@code false} if the in-call UI should be locked in portrait.
+ */
+ public void setInCallAllowsOrientationChange(boolean allowOrientationChange) {
+ if (mInCallActivity == null) {
+ Log.e(this, "InCallActivity is null. Can't set requested orientation.");
+ return;
+ }
+ mInCallActivity.setAllowOrientationChange(allowOrientationChange);
+ }
+
+ public void enableScreenTimeout(boolean enable) {
+ Log.v(this, "enableScreenTimeout: value=" + enable);
+ if (mInCallActivity == null) {
+ Log.e(this, "enableScreenTimeout: InCallActivity is null.");
+ return;
+ }
+
+ final Window window = mInCallActivity.getWindow();
+ if (enable) {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ } else {
+ window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+ }
+
+ /**
+ * Hides or shows the conference manager fragment.
+ *
+ * @param show {@code true} if the conference manager should be shown, {@code false} if it should
+ * be hidden.
+ */
+ public void showConferenceCallManager(boolean show) {
+ if (mInCallActivity != null) {
+ mInCallActivity.showConferenceFragment(show);
+ }
+ if (!show && mManageConferenceActivity != null) {
+ mManageConferenceActivity.finish();
+ }
+ }
+
+ /**
+ * Determines if the dialpad is visible.
+ *
+ * @return {@code true} if the dialpad is visible, {@code false} otherwise.
+ */
+ public boolean isDialpadVisible() {
+ if (mInCallActivity == null) {
+ return false;
+ }
+ return mInCallActivity.isDialpadVisible();
+ }
+
+ public ThemeColorManager getThemeColorManager() {
+ return mThemeColorManager;
+ }
+
+ /** Called when the foreground call changes. */
+ public void onForegroundCallChanged(DialerCall newForegroundCall) {
+ mThemeColorManager.onForegroundCallChanged(mContext, newForegroundCall);
+ if (mInCallActivity != null) {
+ mInCallActivity.onForegroundCallChanged(newForegroundCall);
+ }
+ }
+
+ public InCallActivity getActivity() {
+ return mInCallActivity;
+ }
+
+ /** Called when the UI begins, and starts the callstate callbacks if necessary. */
+ public void setActivity(InCallActivity inCallActivity) {
+ if (inCallActivity == null) {
+ throw new IllegalArgumentException("registerActivity cannot be called with null");
+ }
+ if (mInCallActivity != null && mInCallActivity != inCallActivity) {
+ Log.w(this, "Setting a second activity before destroying the first.");
+ }
+ updateActivity(inCallActivity);
+ }
+
+ ExternalCallNotifier getExternalCallNotifier() {
+ return mExternalCallNotifier;
+ }
+
+ VideoSurfaceTexture getLocalVideoSurfaceTexture() {
+ if (mLocalVideoSurfaceTexture == null) {
+ mLocalVideoSurfaceTexture = VideoSurfaceBindings.createLocalVideoSurfaceTexture();
+ }
+ return mLocalVideoSurfaceTexture;
+ }
+
+ VideoSurfaceTexture getRemoteVideoSurfaceTexture() {
+ if (mRemoteVideoSurfaceTexture == null) {
+ mRemoteVideoSurfaceTexture = VideoSurfaceBindings.createRemoteVideoSurfaceTexture();
+ }
+ return mRemoteVideoSurfaceTexture;
+ }
+
+ void cleanupSurfaces() {
+ if (mRemoteVideoSurfaceTexture != null) {
+ mRemoteVideoSurfaceTexture.setDoneWithSurface();
+ mRemoteVideoSurfaceTexture = null;
+ }
+ if (mLocalVideoSurfaceTexture != null) {
+ mLocalVideoSurfaceTexture.setDoneWithSurface();
+ mLocalVideoSurfaceTexture = null;
+ }
+ }
+
+ /** All the main states of InCallActivity. */
+ public enum InCallState {
+ // InCall Screen is off and there are no calls
+ NO_CALLS,
+
+ // Incoming-call screen is up
+ INCOMING,
+
+ // In-call experience is showing
+ INCALL,
+
+ // Waiting for user input before placing outgoing call
+ WAITING_FOR_ACCOUNT,
+
+ // UI is starting up but no call has been initiated yet.
+ // The UI is waiting for Telecom to respond.
+ PENDING_OUTGOING,
+
+ // User is dialing out
+ OUTGOING;
+
+ public boolean isIncoming() {
+ return (this == INCOMING);
+ }
+
+ public boolean isConnectingOrConnected() {
+ return (this == INCOMING || this == OUTGOING || this == INCALL);
+ }
+ }
+
+ /** Interface implemented by classes that need to know about the InCall State. */
+ public interface InCallStateListener {
+
+ // TODO: Enhance state to contain the call objects instead of passing CallList
+ void onStateChange(InCallState oldState, InCallState newState, CallList callList);
+ }
+
+ public interface IncomingCallListener {
+
+ void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call);
+ }
+
+ public interface CanAddCallListener {
+
+ void onCanAddCallChanged(boolean canAddCall);
+ }
+
+ public interface InCallDetailsListener {
+
+ void onDetailsChanged(DialerCall call, android.telecom.Call.Details details);
+ }
+
+ public interface InCallOrientationListener {
+
+ void onDeviceOrientationChanged(@ScreenOrientation int orientation);
+ }
+
+ /**
+ * Interface implemented by classes that need to know about events which occur within the In-Call
+ * UI. Used as a means of communicating between fragments that make up the UI.
+ */
+ public interface InCallEventListener {
+
+ void onFullscreenModeChanged(boolean isFullscreenMode);
+ }
+
+ public interface InCallUiListener {
+
+ void onUiShowing(boolean showing);
+ }
+}
diff --git a/java/com/android/incallui/InCallServiceImpl.java b/java/com/android/incallui/InCallServiceImpl.java
new file mode 100644
index 000000000..33e8393ae
--- /dev/null
+++ b/java/com/android/incallui/InCallServiceImpl.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.telecom.Call;
+import android.telecom.CallAudioState;
+import android.telecom.InCallService;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.ExternalCallList;
+import com.android.incallui.call.TelecomAdapter;
+
+/**
+ * Used to receive updates about calls from the Telecom component. This service is bound to Telecom
+ * while there exist calls which potentially require UI. This includes ringing (incoming), dialing
+ * (outgoing), and active calls. When the last call is disconnected, Telecom will unbind to the
+ * service triggering InCallActivity (via CallList) to finish soon after.
+ */
+public class InCallServiceImpl extends InCallService {
+
+ @Override
+ public void onCallAudioStateChanged(CallAudioState audioState) {
+ AudioModeProvider.getInstance().onAudioStateChanged(audioState);
+ }
+
+ @Override
+ public void onBringToForeground(boolean showDialpad) {
+ InCallPresenter.getInstance().onBringToForeground(showDialpad);
+ }
+
+ @Override
+ public void onCallAdded(Call call) {
+ InCallPresenter.getInstance().onCallAdded(call);
+ }
+
+ @Override
+ public void onCallRemoved(Call call) {
+ InCallPresenter.getInstance().onCallRemoved(call);
+ }
+
+ @Override
+ public void onCanAddCallChanged(boolean canAddCall) {
+ InCallPresenter.getInstance().onCanAddCallChanged(canAddCall);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ final Context context = getApplicationContext();
+ final ContactInfoCache contactInfoCache = ContactInfoCache.getInstance(context);
+ InCallPresenter.getInstance()
+ .setUp(
+ getApplicationContext(),
+ CallList.getInstance(),
+ new ExternalCallList(),
+ new StatusBarNotifier(context, contactInfoCache),
+ new ExternalCallNotifier(context, contactInfoCache),
+ contactInfoCache,
+ new ProximitySensor(
+ context, AudioModeProvider.getInstance(), new AccelerometerListener(context)));
+ InCallPresenter.getInstance().onServiceBind();
+ InCallPresenter.getInstance().maybeStartRevealAnimation(intent);
+ TelecomAdapter.getInstance().setInCallService(this);
+
+ return super.onBind(intent);
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ super.onUnbind(intent);
+
+ InCallPresenter.getInstance().onServiceUnbind();
+ tearDown();
+
+ return false;
+ }
+
+ private void tearDown() {
+ Log.v(this, "tearDown");
+ // Tear down the InCall system
+ TelecomAdapter.getInstance().clearInCallService();
+ InCallPresenter.getInstance().tearDown();
+ }
+}
diff --git a/java/com/android/incallui/InCallUIMaterialColorMapUtils.java b/java/com/android/incallui/InCallUIMaterialColorMapUtils.java
new file mode 100644
index 000000000..7b06a5e39
--- /dev/null
+++ b/java/com/android/incallui/InCallUIMaterialColorMapUtils.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui;
+
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.telecom.PhoneAccount;
+import com.android.contacts.common.util.MaterialColorMapUtils;
+
+public class InCallUIMaterialColorMapUtils extends MaterialColorMapUtils {
+
+ private final TypedArray mPrimaryColors;
+ private final TypedArray mSecondaryColors;
+ private final Resources mResources;
+
+ public InCallUIMaterialColorMapUtils(Resources resources) {
+ super(resources);
+ mPrimaryColors = resources.obtainTypedArray(R.array.background_colors);
+ mSecondaryColors = resources.obtainTypedArray(R.array.background_colors_dark);
+ mResources = resources;
+ }
+
+ /**
+ * {@link Resources#getColor(int) used for compatibility
+ */
+ @SuppressWarnings("deprecation")
+ public static MaterialPalette getDefaultPrimaryAndSecondaryColors(Resources resources) {
+ final int primaryColor = resources.getColor(R.color.dialer_theme_color);
+ final int secondaryColor = resources.getColor(R.color.dialer_theme_color_dark);
+ return new MaterialPalette(primaryColor, secondaryColor);
+ }
+
+ /**
+ * Currently the InCallUI color will only vary by SIM color which is a list of colors defined in
+ * the background_colors array, so first search the list for the matching color and fall back to
+ * the closest matching color if an exact match does not exist.
+ */
+ @Override
+ public MaterialPalette calculatePrimaryAndSecondaryColor(int color) {
+ if (color == PhoneAccount.NO_HIGHLIGHT_COLOR) {
+ return getDefaultPrimaryAndSecondaryColors(mResources);
+ }
+
+ for (int i = 0; i < mPrimaryColors.length(); i++) {
+ if (mPrimaryColors.getColor(i, 0) == color) {
+ return new MaterialPalette(mPrimaryColors.getColor(i, 0), mSecondaryColors.getColor(i, 0));
+ }
+ }
+
+ // The color isn't in the list, so use the superclass to find an approximate color.
+ return super.calculatePrimaryAndSecondaryColor(color);
+ }
+}
diff --git a/java/com/android/incallui/Log.java b/java/com/android/incallui/Log.java
new file mode 100644
index 000000000..c63eccbd4
--- /dev/null
+++ b/java/com/android/incallui/Log.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui;
+
+import android.net.Uri;
+import android.telecom.PhoneAccount;
+import android.telephony.PhoneNumberUtils;
+import com.android.dialer.common.LogUtil;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/** Manages logging for the entire class. */
+public class Log {
+
+ public static void d(String tag, String msg) {
+ LogUtil.d(tag, msg);
+ }
+
+ public static void d(Object obj, String msg) {
+ LogUtil.d(getPrefix(obj), msg);
+ }
+
+ public static void d(Object obj, String str1, Object str2) {
+ LogUtil.d(getPrefix(obj), str1 + str2);
+ }
+
+ public static void v(Object obj, String msg) {
+ LogUtil.v(getPrefix(obj), msg);
+ }
+
+ public static void v(Object obj, String str1, Object str2) {
+ LogUtil.v(getPrefix(obj), str1 + str2);
+ }
+
+ public static void e(String tag, String msg, Exception e) {
+ LogUtil.e(tag, msg, e);
+ }
+
+ public static void e(String tag, String msg) {
+ LogUtil.e(tag, msg);
+ }
+
+ public static void e(Object obj, String msg, Exception e) {
+ LogUtil.e(getPrefix(obj), msg, e);
+ }
+
+ public static void e(Object obj, String msg) {
+ LogUtil.e(getPrefix(obj), msg);
+ }
+
+ public static void i(String tag, String msg) {
+ LogUtil.i(tag, msg);
+ }
+
+ public static void i(Object obj, String msg) {
+ LogUtil.i(getPrefix(obj), msg);
+ }
+
+ public static void w(Object obj, String msg) {
+ LogUtil.w(getPrefix(obj), msg);
+ }
+
+ public static String piiHandle(Object pii) {
+ if (pii == null || LogUtil.isVerboseEnabled()) {
+ return String.valueOf(pii);
+ }
+
+ if (pii instanceof Uri) {
+ Uri uri = (Uri) pii;
+
+ // All Uri's which are not "tel" go through normal pii() method.
+ if (!PhoneAccount.SCHEME_TEL.equals(uri.getScheme())) {
+ return pii(pii);
+ } else {
+ pii = uri.getSchemeSpecificPart();
+ }
+ }
+
+ String originalString = String.valueOf(pii);
+ StringBuilder stringBuilder = new StringBuilder(originalString.length());
+ for (char c : originalString.toCharArray()) {
+ if (PhoneNumberUtils.isDialable(c)) {
+ stringBuilder.append('*');
+ } else {
+ stringBuilder.append(c);
+ }
+ }
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Redact personally identifiable information for production users. If we are running in verbose
+ * mode, return the original string, otherwise return a SHA-1 hash of the input string.
+ */
+ public static String pii(Object pii) {
+ if (pii == null || LogUtil.isVerboseEnabled()) {
+ return String.valueOf(pii);
+ }
+ return "[" + secureHash(String.valueOf(pii).getBytes()) + "]";
+ }
+
+ private static String secureHash(byte[] input) {
+ MessageDigest messageDigest;
+ try {
+ messageDigest = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ }
+ messageDigest.update(input);
+ byte[] result = messageDigest.digest();
+ return encodeHex(result);
+ }
+
+ private static String encodeHex(byte[] bytes) {
+ StringBuffer hex = new StringBuffer(bytes.length * 2);
+
+ for (int i = 0; i < bytes.length; i++) {
+ int byteIntValue = bytes[i] & 0xff;
+ if (byteIntValue < 0x10) {
+ hex.append("0");
+ }
+ hex.append(Integer.toString(byteIntValue, 16));
+ }
+
+ return hex.toString();
+ }
+
+ private static String getPrefix(Object obj) {
+ return (obj == null ? "" : (obj.getClass().getSimpleName()));
+ }
+}
diff --git a/java/com/android/incallui/ManageConferenceActivity.java b/java/com/android/incallui/ManageConferenceActivity.java
new file mode 100644
index 000000000..6584e4f67
--- /dev/null
+++ b/java/com/android/incallui/ManageConferenceActivity.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+
+/** Shows the {@link ConferenceManagerFragment} */
+public class ManageConferenceActivity extends AppCompatActivity {
+
+ private boolean isVisible;
+
+ public boolean isVisible() {
+ return isVisible;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ InCallPresenter.getInstance().setManageConferenceActivity(this);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ setContentView(R.layout.activity_manage_conference);
+ Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.manageConferencePanel);
+ if (fragment == null) {
+ fragment = new ConferenceManagerFragment();
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(R.id.manageConferencePanel, fragment)
+ .commit();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (isFinishing()) {
+ InCallPresenter.getInstance().setManageConferenceActivity(null);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onBackPressed() {
+ InCallPresenter.getInstance().bringToForeground(false);
+ finish();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ isVisible = true;
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ isVisible = false;
+ }
+}
diff --git a/java/com/android/incallui/NotificationBroadcastReceiver.java b/java/com/android/incallui/NotificationBroadcastReceiver.java
new file mode 100644
index 000000000..5c5d255cc
--- /dev/null
+++ b/java/com/android/incallui/NotificationBroadcastReceiver.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.RequiresApi;
+import android.telecom.VideoProfile;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.VideoUtils;
+
+/**
+ * Accepts broadcast Intents which will be prepared by {@link StatusBarNotifier} and thus sent from
+ * the notification manager. This should be visible from outside, but shouldn't be exported.
+ */
+public class NotificationBroadcastReceiver extends BroadcastReceiver {
+
+ /**
+ * Intent Action used for hanging up the current call from Notification bar. This will choose
+ * first ringing call, first active call, or first background call (typically in STATE_HOLDING
+ * state).
+ */
+ public static final String ACTION_DECLINE_INCOMING_CALL =
+ "com.android.incallui.ACTION_DECLINE_INCOMING_CALL";
+
+ public static final String ACTION_HANG_UP_ONGOING_CALL =
+ "com.android.incallui.ACTION_HANG_UP_ONGOING_CALL";
+ public static final String ACTION_ANSWER_VIDEO_INCOMING_CALL =
+ "com.android.incallui.ACTION_ANSWER_VIDEO_INCOMING_CALL";
+ public static final String ACTION_ANSWER_VOICE_INCOMING_CALL =
+ "com.android.incallui.ACTION_ANSWER_VOICE_INCOMING_CALL";
+ public static final String ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST =
+ "com.android.incallui.ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST";
+ public static final String ACTION_DECLINE_VIDEO_UPGRADE_REQUEST =
+ "com.android.incallui.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST";
+
+ @RequiresApi(VERSION_CODES.N_MR1)
+ public static final String ACTION_PULL_EXTERNAL_CALL =
+ "com.android.incallui.ACTION_PULL_EXTERNAL_CALL";
+
+ public static final String EXTRA_NOTIFICATION_ID =
+ "com.android.incallui.extra.EXTRA_NOTIFICATION_ID";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ LogUtil.i("NotificationBroadcastReceiver.onReceive", "Broadcast from Notification: " + action);
+
+ // TODO: Commands of this nature should exist in the CallList.
+ if (action.equals(ACTION_ANSWER_VIDEO_INCOMING_CALL)) {
+ answerIncomingCall(context, VideoProfile.STATE_BIDIRECTIONAL);
+ } else if (action.equals(ACTION_ANSWER_VOICE_INCOMING_CALL)) {
+ answerIncomingCall(context, VideoProfile.STATE_AUDIO_ONLY);
+ } else if (action.equals(ACTION_DECLINE_INCOMING_CALL)) {
+ Logger.get(context)
+ .logImpression(DialerImpression.Type.REJECT_INCOMING_CALL_FROM_NOTIFICATION);
+ declineIncomingCall(context);
+ } else if (action.equals(ACTION_HANG_UP_ONGOING_CALL)) {
+ hangUpOngoingCall(context);
+ } else if (action.equals(ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST)) {
+ acceptUpgradeRequest(context);
+ } else if (action.equals(ACTION_DECLINE_VIDEO_UPGRADE_REQUEST)) {
+ declineUpgradeRequest(context);
+ } else if (action.equals(ACTION_PULL_EXTERNAL_CALL)) {
+ context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+ int notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1);
+ InCallPresenter.getInstance().getExternalCallNotifier().pullExternalCall(notificationId);
+ }
+ }
+
+ private void acceptUpgradeRequest(Context context) {
+ CallList callList = InCallPresenter.getInstance().getCallList();
+ if (callList == null) {
+ StatusBarNotifier.clearAllCallNotifications(context);
+ LogUtil.e("NotificationBroadcastReceiver.acceptUpgradeRequest", "call list is empty");
+ } else {
+ DialerCall call = callList.getVideoUpgradeRequestCall();
+ if (call != null) {
+ call.acceptUpgradeRequest(call.getRequestedVideoState());
+ }
+ }
+ }
+
+ private void declineUpgradeRequest(Context context) {
+ CallList callList = InCallPresenter.getInstance().getCallList();
+ if (callList == null) {
+ StatusBarNotifier.clearAllCallNotifications(context);
+ LogUtil.e("NotificationBroadcastReceiver.declineUpgradeRequest", "call list is empty");
+ } else {
+ DialerCall call = callList.getVideoUpgradeRequestCall();
+ if (call != null) {
+ call.declineUpgradeRequest();
+ }
+ }
+ }
+
+ private void hangUpOngoingCall(Context context) {
+ CallList callList = InCallPresenter.getInstance().getCallList();
+ if (callList == null) {
+ StatusBarNotifier.clearAllCallNotifications(context);
+ LogUtil.e("NotificationBroadcastReceiver.hangUpOngoingCall", "call list is empty");
+ } else {
+ DialerCall call = callList.getOutgoingCall();
+ if (call == null) {
+ call = callList.getActiveOrBackgroundCall();
+ }
+ LogUtil.i(
+ "NotificationBroadcastReceiver.hangUpOngoingCall", "disconnecting call, call: " + call);
+ if (call != null) {
+ call.disconnect();
+ }
+ }
+ }
+
+ private void answerIncomingCall(Context context, int videoState) {
+ CallList callList = InCallPresenter.getInstance().getCallList();
+ if (callList == null) {
+ StatusBarNotifier.clearAllCallNotifications(context);
+ LogUtil.e("NotificationBroadcastReceiver.answerIncomingCall", "call list is empty");
+ } else {
+ DialerCall call = callList.getIncomingCall();
+ if (call != null) {
+ call.answer(videoState);
+ InCallPresenter.getInstance()
+ .showInCall(
+ false /* showDialpad */,
+ false /* newOutgoingCall */,
+ VideoUtils.isVideoCall(videoState));
+ }
+ }
+ }
+
+ private void declineIncomingCall(Context context) {
+ CallList callList = InCallPresenter.getInstance().getCallList();
+ if (callList == null) {
+ StatusBarNotifier.clearAllCallNotifications(context);
+ LogUtil.e("NotificationBroadcastReceiver.declineIncomingCall", "call list is empty");
+ } else {
+ DialerCall call = callList.getIncomingCall();
+ if (call != null) {
+ call.reject(false /* rejectWithMessage */, null);
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/PostCharDialogFragment.java b/java/com/android/incallui/PostCharDialogFragment.java
new file mode 100644
index 000000000..a852f7683
--- /dev/null
+++ b/java/com/android/incallui/PostCharDialogFragment.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import com.android.incallui.call.TelecomAdapter;
+
+/**
+ * Pop up an alert dialog with OK and Cancel buttons to allow user to Accept or Reject the WAIT
+ * inserted as part of the Dial string.
+ */
+public class PostCharDialogFragment extends DialogFragment {
+
+ private static final String STATE_CALL_ID = "CALL_ID";
+ private static final String STATE_POST_CHARS = "POST_CHARS";
+
+ private String mCallId;
+ private String mPostDialStr;
+
+ public PostCharDialogFragment() {}
+
+ public PostCharDialogFragment(String callId, String postDialStr) {
+ mCallId = callId;
+ mPostDialStr = postDialStr;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ super.onCreateDialog(savedInstanceState);
+
+ if (mPostDialStr == null && savedInstanceState != null) {
+ mCallId = savedInstanceState.getString(STATE_CALL_ID);
+ mPostDialStr = savedInstanceState.getString(STATE_POST_CHARS);
+ }
+
+ final StringBuilder buf = new StringBuilder();
+ buf.append(getResources().getText(R.string.wait_prompt_str));
+ buf.append(mPostDialStr);
+
+ final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setMessage(buf.toString());
+
+ builder.setPositiveButton(
+ R.string.pause_prompt_yes,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ TelecomAdapter.getInstance().postDialContinue(mCallId, true);
+ }
+ });
+ builder.setNegativeButton(
+ R.string.pause_prompt_no,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ dialog.cancel();
+ }
+ });
+
+ final AlertDialog dialog = builder.create();
+ return dialog;
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ super.onCancel(dialog);
+
+ TelecomAdapter.getInstance().postDialContinue(mCallId, false);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ outState.putString(STATE_CALL_ID, mCallId);
+ outState.putString(STATE_POST_CHARS, mPostDialStr);
+ }
+}
diff --git a/java/com/android/incallui/ProximitySensor.java b/java/com/android/incallui/ProximitySensor.java
new file mode 100644
index 000000000..91220627c
--- /dev/null
+++ b/java/com/android/incallui/ProximitySensor.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.DisplayListener;
+import android.os.PowerManager;
+import android.support.annotation.NonNull;
+import android.telecom.CallAudioState;
+import android.view.Display;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.AudioModeProvider.AudioModeListener;
+import com.android.incallui.InCallPresenter.InCallState;
+import com.android.incallui.InCallPresenter.InCallStateListener;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.VideoUtils;
+
+/**
+ * Class manages the proximity sensor for the in-call UI. We enable the proximity sensor while the
+ * user in a phone call. The Proximity sensor turns off the touchscreen and display when the user is
+ * close to the screen to prevent user's cheek from causing touch events. The class requires special
+ * knowledge of the activity and device state to know when the proximity sensor should be enabled
+ * and disabled. Most of that state is fed into this class through public methods.
+ */
+public class ProximitySensor
+ implements AccelerometerListener.OrientationListener, InCallStateListener, AudioModeListener {
+
+ private static final String TAG = ProximitySensor.class.getSimpleName();
+
+ private final PowerManager mPowerManager;
+ private final PowerManager.WakeLock mProximityWakeLock;
+ private final AudioModeProvider mAudioModeProvider;
+ private final AccelerometerListener mAccelerometerListener;
+ private final ProximityDisplayListener mDisplayListener;
+ private int mOrientation = AccelerometerListener.ORIENTATION_UNKNOWN;
+ private boolean mUiShowing = false;
+ private boolean mIsPhoneOffhook = false;
+ private boolean mDialpadVisible;
+ private boolean mIsAttemptingVideoCall;
+ private boolean mIsVideoCall;
+
+ public ProximitySensor(
+ @NonNull Context context,
+ @NonNull AudioModeProvider audioModeProvider,
+ @NonNull AccelerometerListener accelerometerListener) {
+ mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ if (mPowerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
+ mProximityWakeLock =
+ mPowerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
+ } else {
+ LogUtil.i("ProximitySensor.constructor", "Device does not support proximity wake lock.");
+ mProximityWakeLock = null;
+ }
+ mAccelerometerListener = accelerometerListener;
+ mAccelerometerListener.setListener(this);
+
+ mDisplayListener =
+ new ProximityDisplayListener(
+ (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE));
+ mDisplayListener.register();
+
+ mAudioModeProvider = audioModeProvider;
+ mAudioModeProvider.addListener(this);
+ }
+
+ public void tearDown() {
+ mAudioModeProvider.removeListener(this);
+
+ mAccelerometerListener.enable(false);
+ mDisplayListener.unregister();
+
+ turnOffProximitySensor(true);
+ }
+
+ /** Called to identify when the device is laid down flat. */
+ @Override
+ public void orientationChanged(int orientation) {
+ mOrientation = orientation;
+ updateProximitySensorMode();
+ }
+
+ /** Called to keep track of the overall UI state. */
+ @Override
+ public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
+ // We ignore incoming state because we do not want to enable proximity
+ // sensor during incoming call screen. We check hasLiveCall() because a disconnected call
+ // can also put the in-call screen in the INCALL state.
+ boolean hasOngoingCall = InCallState.INCALL == newState && callList.hasLiveCall();
+ boolean isOffhook = (InCallState.OUTGOING == newState) || hasOngoingCall;
+
+ boolean isVideoCall = VideoUtils.isVideoCall(callList.getActiveCall());
+
+ if (isOffhook != mIsPhoneOffhook || mIsVideoCall != isVideoCall) {
+ mIsPhoneOffhook = isOffhook;
+ mIsVideoCall = isVideoCall;
+
+ mOrientation = AccelerometerListener.ORIENTATION_UNKNOWN;
+ mAccelerometerListener.enable(mIsPhoneOffhook);
+
+ updateProximitySensorMode();
+ }
+ }
+
+ @Override
+ public void onAudioStateChanged(CallAudioState audioState) {
+ updateProximitySensorMode();
+ }
+
+ public void onDialpadVisible(boolean visible) {
+ mDialpadVisible = visible;
+ updateProximitySensorMode();
+ }
+
+ public void setIsAttemptingVideoCall(boolean isAttemptingVideoCall) {
+ LogUtil.i(
+ "ProximitySensor.setIsAttemptingVideoCall",
+ "isAttemptingVideoCall: %b",
+ isAttemptingVideoCall);
+ mIsAttemptingVideoCall = isAttemptingVideoCall;
+ updateProximitySensorMode();
+ }
+ /** Used to save when the UI goes in and out of the foreground. */
+ public void onInCallShowing(boolean showing) {
+ if (showing) {
+ mUiShowing = true;
+
+ // We only consider the UI not showing for instances where another app took the foreground.
+ // If we stopped showing because the screen is off, we still consider that showing.
+ } else if (mPowerManager.isScreenOn()) {
+ mUiShowing = false;
+ }
+ updateProximitySensorMode();
+ }
+
+ void onDisplayStateChanged(boolean isDisplayOn) {
+ LogUtil.i("ProximitySensor.onDisplayStateChanged", "isDisplayOn: %b", isDisplayOn);
+ mAccelerometerListener.enable(isDisplayOn);
+ }
+
+ /**
+ * TODO: There is no way to determine if a screen is off due to proximity or if it is legitimately
+ * off, but if ever we can do that in the future, it would be useful here. Until then, this
+ * function will simply return true of the screen is off. TODO: Investigate whether this can be
+ * replaced with the ProximityDisplayListener.
+ */
+ public boolean isScreenReallyOff() {
+ return !mPowerManager.isScreenOn();
+ }
+
+ private void turnOnProximitySensor() {
+ if (mProximityWakeLock != null) {
+ if (!mProximityWakeLock.isHeld()) {
+ LogUtil.i("ProximitySensor.turnOnProximitySensor", "acquiring wake lock");
+ mProximityWakeLock.acquire();
+ } else {
+ LogUtil.i("ProximitySensor.turnOnProximitySensor", "wake lock already acquired");
+ }
+ }
+ }
+
+ private void turnOffProximitySensor(boolean screenOnImmediately) {
+ if (mProximityWakeLock != null) {
+ if (mProximityWakeLock.isHeld()) {
+ LogUtil.i("ProximitySensor.turnOffProximitySensor", "releasing wake lock");
+ int flags = (screenOnImmediately ? 0 : PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
+ mProximityWakeLock.release(flags);
+ } else {
+ LogUtil.i("ProximitySensor.turnOffProximitySensor", "wake lock already released");
+ }
+ }
+ }
+
+ /**
+ * Updates the wake lock used to control proximity sensor behavior, based on the current state of
+ * the phone.
+ *
+ * <p>On devices that have a proximity sensor, to avoid false touches during a call, we hold a
+ * PROXIMITY_SCREEN_OFF_WAKE_LOCK wake lock whenever the phone is off hook. (When held, that wake
+ * lock causes the screen to turn off automatically when the sensor detects an object close to the
+ * screen.)
+ *
+ * <p>This method is a no-op for devices that don't have a proximity sensor.
+ *
+ * <p>Proximity wake lock will be released if any of the following conditions are true: the audio
+ * is routed through bluetooth, a wired headset, or the speaker; the user requested, received a
+ * request for, or is in a video call; or the phone is horizontal while in a call.
+ */
+ private synchronized void updateProximitySensorMode() {
+ final int audioRoute = mAudioModeProvider.getAudioState().getRoute();
+
+ boolean screenOnImmediately =
+ (CallAudioState.ROUTE_WIRED_HEADSET == audioRoute
+ || CallAudioState.ROUTE_SPEAKER == audioRoute
+ || CallAudioState.ROUTE_BLUETOOTH == audioRoute
+ || mIsAttemptingVideoCall
+ || mIsVideoCall);
+
+ // We do not keep the screen off when the user is outside in-call screen and we are
+ // horizontal, but we do not force it on when we become horizontal until the
+ // proximity sensor goes negative.
+ final boolean horizontal = (mOrientation == AccelerometerListener.ORIENTATION_HORIZONTAL);
+ screenOnImmediately |= !mUiShowing && horizontal;
+
+ // We do not keep the screen off when dialpad is visible, we are horizontal, and
+ // the in-call screen is being shown.
+ // At that moment we're pretty sure users want to use it, instead of letting the
+ // proximity sensor turn off the screen by their hands.
+ screenOnImmediately |= mDialpadVisible && horizontal;
+
+ LogUtil.i(
+ "ProximitySensor.updateProximitySensorMode",
+ "screenOnImmediately: %b, dialPadVisible: %b, "
+ + "offHook: %b, horizontal: %b, uiShowing: %b, audioRoute: %s",
+ screenOnImmediately,
+ mDialpadVisible,
+ mIsPhoneOffhook,
+ mOrientation == AccelerometerListener.ORIENTATION_HORIZONTAL,
+ mUiShowing,
+ CallAudioState.audioRouteToString(audioRoute));
+
+ if (mIsPhoneOffhook && !screenOnImmediately) {
+ LogUtil.v("ProximitySensor.updateProximitySensorMode", "turning on proximity sensor");
+ // Phone is in use! Arrange for the screen to turn off
+ // automatically when the sensor detects a close object.
+ turnOnProximitySensor();
+ } else {
+ LogUtil.v("ProximitySensor.updateProximitySensorMode", "turning off proximity sensor");
+ // Phone is either idle, or ringing. We don't want any special proximity sensor
+ // behavior in either case.
+ turnOffProximitySensor(screenOnImmediately);
+ }
+ }
+
+ /**
+ * Implementation of a {@link DisplayListener} that maintains a binary state: Screen on vs screen
+ * off. Used by the proximity sensor manager to decide whether or not it needs to listen to
+ * accelerometer events.
+ */
+ public class ProximityDisplayListener implements DisplayListener {
+
+ private DisplayManager mDisplayManager;
+ private boolean mIsDisplayOn = true;
+
+ ProximityDisplayListener(DisplayManager displayManager) {
+ mDisplayManager = displayManager;
+ }
+
+ void register() {
+ mDisplayManager.registerDisplayListener(this, null);
+ }
+
+ void unregister() {
+ mDisplayManager.unregisterDisplayListener(this);
+ }
+
+ @Override
+ public void onDisplayRemoved(int displayId) {}
+
+ @Override
+ public void onDisplayChanged(int displayId) {
+ if (displayId == Display.DEFAULT_DISPLAY) {
+ final Display display = mDisplayManager.getDisplay(displayId);
+
+ final boolean isDisplayOn = display.getState() != Display.STATE_OFF;
+ // For call purposes, we assume that as long as the screen is not truly off, it is
+ // considered on, even if it is in an unknown or low power idle state.
+ if (isDisplayOn != mIsDisplayOn) {
+ mIsDisplayOn = isDisplayOn;
+ onDisplayStateChanged(mIsDisplayOn);
+ }
+ }
+ }
+
+ @Override
+ public void onDisplayAdded(int displayId) {}
+ }
+}
diff --git a/java/com/android/incallui/StatusBarNotifier.java b/java/com/android/incallui/StatusBarNotifier.java
new file mode 100644
index 000000000..c7226753f
--- /dev/null
+++ b/java/com/android/incallui/StatusBarNotifier.java
@@ -0,0 +1,842 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui;
+
+import static com.android.contacts.common.compat.CallCompat.Details.PROPERTY_ENTERPRISE_CALL;
+import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST;
+import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VIDEO_INCOMING_CALL;
+import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VOICE_INCOMING_CALL;
+import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_INCOMING_CALL;
+import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST;
+import static com.android.incallui.NotificationBroadcastReceiver.ACTION_HANG_UP_ONGOING_CALL;
+
+import android.app.ActivityManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.AudioAttributes;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.ColorRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
+import android.support.annotation.VisibleForTesting;
+import android.telecom.Call.Details;
+import android.telecom.PhoneAccount;
+import android.telecom.TelecomManager;
+import android.text.BidiFormatter;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.ContactsUtils.UserType;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.common.util.BitmapUtil;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.DrawableConverter;
+import com.android.incallui.ContactInfoCache.ContactCacheEntry;
+import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
+import com.android.incallui.InCallPresenter.InCallState;
+import com.android.incallui.async.PausableExecutorImpl;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.call.DialerCallListener;
+import com.android.incallui.ringtone.DialerRingtoneManager;
+import com.android.incallui.ringtone.InCallTonePlayer;
+import com.android.incallui.ringtone.ToneGeneratorFactory;
+import java.util.Objects;
+
+/** This class adds Notifications to the status bar for the in-call experience. */
+public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
+
+ // Notification types
+ // Indicates that no notification is currently showing.
+ private static final int NOTIFICATION_NONE = 0;
+ // Notification for an active call. This is non-interruptive, but cannot be dismissed.
+ private static final int NOTIFICATION_IN_CALL = 1;
+ // Notification for incoming calls. This is interruptive and will show up as a HUN.
+ private static final int NOTIFICATION_INCOMING_CALL = 2;
+
+ private static final int PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN = 0;
+ private static final int PENDING_INTENT_REQUEST_CODE_FULL_SCREEN = 1;
+
+ private static final long[] VIBRATE_PATTERN = new long[] {0, 1000, 1000};
+
+ private final Context mContext;
+ private final ContactInfoCache mContactInfoCache;
+ private final NotificationManager mNotificationManager;
+ private final DialerRingtoneManager mDialerRingtoneManager;
+ @Nullable private ContactsPreferences mContactsPreferences;
+ private int mCurrentNotification = NOTIFICATION_NONE;
+ private int mCallState = DialerCall.State.INVALID;
+ private int mSavedIcon = 0;
+ private String mSavedContent = null;
+ private Bitmap mSavedLargeIcon;
+ private String mSavedContentTitle;
+ private Uri mRingtone;
+ private StatusBarCallListener mStatusBarCallListener;
+
+ public StatusBarNotifier(@NonNull Context context, @NonNull ContactInfoCache contactInfoCache) {
+ Objects.requireNonNull(context);
+ mContext = context;
+ mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext);
+ mContactInfoCache = contactInfoCache;
+ mNotificationManager = context.getSystemService(NotificationManager.class);
+ mDialerRingtoneManager =
+ new DialerRingtoneManager(
+ new InCallTonePlayer(new ToneGeneratorFactory(), new PausableExecutorImpl()),
+ CallList.getInstance());
+ mCurrentNotification = NOTIFICATION_NONE;
+ }
+
+ /**
+ * Should only be called from a irrecoverable state where it is necessary to dismiss all
+ * notifications.
+ */
+ static void clearAllCallNotifications(Context backupContext) {
+ Log.i(
+ StatusBarNotifier.class.getSimpleName(),
+ "Something terrible happened. Clear all InCall notifications");
+
+ NotificationManager notificationManager =
+ backupContext.getSystemService(NotificationManager.class);
+ notificationManager.cancel(NOTIFICATION_IN_CALL);
+ notificationManager.cancel(NOTIFICATION_INCOMING_CALL);
+ }
+
+ private static int getWorkStringFromPersonalString(int resId) {
+ if (resId == R.string.notification_ongoing_call) {
+ return R.string.notification_ongoing_work_call;
+ } else if (resId == R.string.notification_ongoing_call_wifi) {
+ return R.string.notification_ongoing_work_call_wifi;
+ } else if (resId == R.string.notification_incoming_call_wifi) {
+ return R.string.notification_incoming_work_call_wifi;
+ } else if (resId == R.string.notification_incoming_call) {
+ return R.string.notification_incoming_work_call;
+ } else {
+ return resId;
+ }
+ }
+
+ /**
+ * Returns PendingIntent for answering a phone call. This will typically be used from Notification
+ * context.
+ */
+ private static PendingIntent createNotificationPendingIntent(Context context, String action) {
+ final Intent intent = new Intent(action, null, context, NotificationBroadcastReceiver.class);
+ return PendingIntent.getBroadcast(context, 0, intent, 0);
+ }
+
+ /** Creates notifications according to the state we receive from {@link InCallPresenter}. */
+ @Override
+ public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
+ Log.d(this, "onStateChange");
+ updateNotification(callList);
+ }
+
+ /**
+ * Updates the phone app's status bar notification *and* launches the incoming call UI in response
+ * to a new incoming call.
+ *
+ * <p>If an incoming call is ringing (or call-waiting), the notification will also include a
+ * "fullScreenIntent" that will cause the InCallScreen to be launched, unless the current
+ * foreground activity is marked as "immersive".
+ *
+ * <p>(This is the mechanism that actually brings up the incoming call UI when we receive a "new
+ * ringing connection" event from the telephony layer.)
+ *
+ * <p>Also note that this method is safe to call even if the phone isn't actually ringing (or,
+ * more likely, if an incoming call *was* ringing briefly but then disconnected). In that case,
+ * we'll simply update or cancel the in-call notification based on the current phone state.
+ *
+ * @see #updateInCallNotification(CallList)
+ */
+ public void updateNotification(CallList callList) {
+ updateInCallNotification(callList);
+ }
+
+ /**
+ * Take down the in-call notification.
+ *
+ * @see #updateInCallNotification(CallList)
+ */
+ private void cancelNotification() {
+ if (mStatusBarCallListener != null) {
+ setStatusBarCallListener(null);
+ }
+ if (mCurrentNotification != NOTIFICATION_NONE) {
+ Log.d(this, "cancelInCall()...");
+ mNotificationManager.cancel(mCurrentNotification);
+ }
+ mCurrentNotification = NOTIFICATION_NONE;
+ }
+
+ /**
+ * Helper method for updateInCallNotification() and updateNotification(): Update the phone app's
+ * status bar notification based on the current telephony state, or cancels the notification if
+ * the phone is totally idle.
+ */
+ private void updateInCallNotification(CallList callList) {
+ Log.d(this, "updateInCallNotification...");
+
+ final DialerCall call = getCallToShow(callList);
+
+ if (call != null) {
+ showNotification(callList, call);
+ } else {
+ cancelNotification();
+ }
+ }
+
+ private void showNotification(final CallList callList, final DialerCall call) {
+ final boolean isIncoming =
+ (call.getState() == DialerCall.State.INCOMING
+ || call.getState() == DialerCall.State.CALL_WAITING);
+ setStatusBarCallListener(new StatusBarCallListener(call));
+
+ // we make a call to the contact info cache to query for supplemental data to what the
+ // call provides. This includes the contact name and photo.
+ // This callback will always get called immediately and synchronously with whatever data
+ // it has available, and may make a subsequent call later (same thread) if it had to
+ // call into the contacts provider for more data.
+ mContactInfoCache.findInfo(
+ call,
+ isIncoming,
+ new ContactInfoCacheCallback() {
+ @Override
+ public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
+ DialerCall call = callList.getCallById(callId);
+ if (call != null) {
+ call.getLogState().contactLookupResult = entry.contactLookupResult;
+ buildAndSendNotification(callList, call, entry);
+ }
+ }
+
+ @Override
+ public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
+ DialerCall call = callList.getCallById(callId);
+ if (call != null) {
+ buildAndSendNotification(callList, call, entry);
+ }
+ }
+ });
+ }
+
+ /** Sets up the main Ui for the notification */
+ private void buildAndSendNotification(
+ CallList callList, DialerCall originalCall, ContactCacheEntry contactInfo) {
+ // This can get called to update an existing notification after contact information has come
+ // back. However, it can happen much later. Before we continue, we need to make sure that
+ // the call being passed in is still the one we want to show in the notification.
+ final DialerCall call = getCallToShow(callList);
+ if (call == null || !call.getId().equals(originalCall.getId())) {
+ return;
+ }
+
+ final int callState = call.getState();
+
+ // Check if data has changed; if nothing is different, don't issue another notification.
+ final int iconResId = getIconToDisplay(call);
+ Bitmap largeIcon = getLargeIconToDisplay(contactInfo, call);
+ final String content = getContentString(call, contactInfo.userType);
+ final String contentTitle = getContentTitle(contactInfo, call);
+
+ final boolean isVideoUpgradeRequest =
+ call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
+ final int notificationType;
+ if (callState == DialerCall.State.INCOMING
+ || callState == DialerCall.State.CALL_WAITING
+ || isVideoUpgradeRequest) {
+ notificationType = NOTIFICATION_INCOMING_CALL;
+ } else {
+ notificationType = NOTIFICATION_IN_CALL;
+ }
+
+ if (!checkForChangeAndSaveData(
+ iconResId,
+ content,
+ largeIcon,
+ contentTitle,
+ callState,
+ notificationType,
+ contactInfo.contactRingtoneUri)) {
+ return;
+ }
+
+ if (largeIcon != null) {
+ largeIcon = getRoundedIcon(largeIcon);
+ }
+
+ // This builder is used for the notification shown when the device is locked and the user
+ // has set their notification settings to 'hide sensitive content'
+ // {@see Notification.Builder#setPublicVersion}.
+ Notification.Builder publicBuilder = new Notification.Builder(mContext);
+ publicBuilder
+ .setSmallIcon(iconResId)
+ .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
+ // Hide work call state for the lock screen notification
+ .setContentTitle(getContentString(call, ContactsUtils.USER_TYPE_CURRENT));
+ setNotificationWhen(call, callState, publicBuilder);
+
+ // Builder for the notification shown when the device is unlocked or the user has set their
+ // notification settings to 'show all notification content'.
+ final Notification.Builder builder = getNotificationBuilder();
+ builder.setPublicVersion(publicBuilder.build());
+
+ // Set up the main intent to send the user to the in-call screen
+ builder.setContentIntent(
+ createLaunchPendingIntent(false /* isFullScreen */, call.isVideoCall()));
+
+ // Set the intent as a full screen intent as well if a call is incoming
+ if (notificationType == NOTIFICATION_INCOMING_CALL) {
+ if (!InCallPresenter.getInstance().isActivityStarted()) {
+ configureFullScreenIntent(
+ builder,
+ createLaunchPendingIntent(true /* isFullScreen */, call.isVideoCall()),
+ callList,
+ call);
+ } else {
+ // If the incall screen is already up, we don't want to show HUN but regular notification
+ // should still be shown. In order to do that the previous one with full screen intent
+ // needs to be cancelled.
+ LogUtil.d(
+ "StatusBarNotifier.buildAndSendNotification",
+ "cancel previous incoming call notification");
+ mNotificationManager.cancel(NOTIFICATION_INCOMING_CALL);
+ }
+ // Set the notification category for incoming calls
+ builder.setCategory(Notification.CATEGORY_CALL);
+ }
+
+ // Set the content
+ builder.setContentText(content);
+ builder.setSmallIcon(iconResId);
+ builder.setContentTitle(contentTitle);
+ builder.setLargeIcon(largeIcon);
+ builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
+
+ if (isVideoUpgradeRequest) {
+ builder.setUsesChronometer(false);
+ addDismissUpgradeRequestAction(builder);
+ addAcceptUpgradeRequestAction(builder);
+ } else {
+ createIncomingCallNotification(call, callState, builder);
+ }
+
+ addPersonReference(builder, contactInfo, call);
+
+ // Fire off the notification
+ Notification notification = builder.build();
+
+ if (mDialerRingtoneManager.shouldPlayRingtone(callState, contactInfo.contactRingtoneUri)) {
+ notification.flags |= Notification.FLAG_INSISTENT;
+ notification.sound = contactInfo.contactRingtoneUri;
+ AudioAttributes.Builder audioAttributes = new AudioAttributes.Builder();
+ audioAttributes.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC);
+ audioAttributes.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE);
+ notification.audioAttributes = audioAttributes.build();
+ if (mDialerRingtoneManager.shouldVibrate(mContext.getContentResolver())) {
+ notification.vibrate = VIBRATE_PATTERN;
+ }
+ }
+ if (mDialerRingtoneManager.shouldPlayCallWaitingTone(callState)) {
+ Log.v(this, "Playing call waiting tone");
+ mDialerRingtoneManager.playCallWaitingTone();
+ }
+ if (mCurrentNotification != notificationType && mCurrentNotification != NOTIFICATION_NONE) {
+ Log.i(this, "Previous notification already showing - cancelling " + mCurrentNotification);
+ mNotificationManager.cancel(mCurrentNotification);
+ }
+
+ Log.i(this, "Displaying notification for " + notificationType);
+ try {
+ mNotificationManager.notify(notificationType, notification);
+ } catch (RuntimeException e) {
+ // TODO(b/34744003): Move the memory stats into silent feedback PSD.
+ ActivityManager activityManager = mContext.getSystemService(ActivityManager.class);
+ ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
+ activityManager.getMemoryInfo(memoryInfo);
+ throw new RuntimeException(
+ String.format(
+ "Error displaying notification with photo type: %d (low memory? %b, availMem: %d)",
+ contactInfo.photoType, memoryInfo.lowMemory, memoryInfo.availMem),
+ e);
+ }
+ call.getLatencyReport().onNotificationShown();
+ mCurrentNotification = notificationType;
+ }
+
+ private void createIncomingCallNotification(
+ DialerCall call, int state, Notification.Builder builder) {
+ setNotificationWhen(call, state, builder);
+
+ // Add hang up option for any active calls (active | onhold), outgoing calls (dialing).
+ if (state == DialerCall.State.ACTIVE
+ || state == DialerCall.State.ONHOLD
+ || DialerCall.State.isDialing(state)) {
+ addHangupAction(builder);
+ } else if (state == DialerCall.State.INCOMING || state == DialerCall.State.CALL_WAITING) {
+ addDismissAction(builder);
+ if (call.isVideoCall()) {
+ addVideoCallAction(builder);
+ } else {
+ addAnswerAction(builder);
+ }
+ }
+ }
+
+ /**
+ * Sets the notification's when section as needed. For active calls, this is explicitly set as the
+ * duration of the call. For all other states, the notification will automatically show the time
+ * at which the notification was created.
+ */
+ private void setNotificationWhen(DialerCall call, int state, Notification.Builder builder) {
+ if (state == DialerCall.State.ACTIVE) {
+ builder.setUsesChronometer(true);
+ builder.setWhen(call.getConnectTimeMillis());
+ } else {
+ builder.setUsesChronometer(false);
+ }
+ }
+
+ /**
+ * Checks the new notification data and compares it against any notification that we are already
+ * displaying. If the data is exactly the same, we return false so that we do not issue a new
+ * notification for the exact same data.
+ */
+ private boolean checkForChangeAndSaveData(
+ int icon,
+ String content,
+ Bitmap largeIcon,
+ String contentTitle,
+ int state,
+ int notificationType,
+ Uri ringtone) {
+
+ // The two are different:
+ // if new title is not null, it should be different from saved version OR
+ // if new title is null, the saved version should not be null
+ final boolean contentTitleChanged =
+ (contentTitle != null && !contentTitle.equals(mSavedContentTitle))
+ || (contentTitle == null && mSavedContentTitle != null);
+
+ // any change means we are definitely updating
+ boolean retval =
+ (mSavedIcon != icon)
+ || !Objects.equals(mSavedContent, content)
+ || (mCallState != state)
+ || (mSavedLargeIcon != largeIcon)
+ || contentTitleChanged
+ || !Objects.equals(mRingtone, ringtone);
+
+ // If we aren't showing a notification right now or the notification type is changing,
+ // definitely do an update.
+ if (mCurrentNotification != notificationType) {
+ if (mCurrentNotification == NOTIFICATION_NONE) {
+ Log.d(this, "Showing notification for first time.");
+ }
+ retval = true;
+ }
+
+ mSavedIcon = icon;
+ mSavedContent = content;
+ mCallState = state;
+ mSavedLargeIcon = largeIcon;
+ mSavedContentTitle = contentTitle;
+ mRingtone = ringtone;
+
+ if (retval) {
+ Log.d(this, "Data changed. Showing notification");
+ }
+
+ return retval;
+ }
+
+ /** Returns the main string to use in the notification. */
+ @VisibleForTesting
+ @Nullable
+ String getContentTitle(ContactCacheEntry contactInfo, DialerCall call) {
+ if (call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) {
+ return mContext.getResources().getString(R.string.conference_call_name);
+ }
+
+ String preferredName =
+ ContactDisplayUtils.getPreferredDisplayName(
+ contactInfo.namePrimary, contactInfo.nameAlternative, mContactsPreferences);
+ if (TextUtils.isEmpty(preferredName)) {
+ return TextUtils.isEmpty(contactInfo.number)
+ ? null
+ : BidiFormatter.getInstance()
+ .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR);
+ }
+ return preferredName;
+ }
+
+ private void addPersonReference(
+ Notification.Builder builder, ContactCacheEntry contactInfo, DialerCall call) {
+ // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed.
+ // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid
+ // NotificationManager using it.
+ if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) {
+ builder.addPerson(contactInfo.lookupUri.toString());
+ } else if (!TextUtils.isEmpty(call.getNumber())) {
+ builder.addPerson(Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null).toString());
+ }
+ }
+
+ /** Gets a large icon from the contact info object to display in the notification. */
+ private Bitmap getLargeIconToDisplay(ContactCacheEntry contactInfo, DialerCall call) {
+ Bitmap largeIcon = null;
+ if (call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) {
+ largeIcon = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.img_conference);
+ }
+ if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) {
+ largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap();
+ }
+ if (call.isSpam()) {
+ Drawable drawable = mContext.getResources().getDrawable(R.drawable.blocked_contact);
+ largeIcon = DrawableConverter.drawableToBitmap(drawable);
+ }
+ return largeIcon;
+ }
+
+ private Bitmap getRoundedIcon(Bitmap bitmap) {
+ if (bitmap == null) {
+ return null;
+ }
+ final int height =
+ (int) mContext.getResources().getDimension(android.R.dimen.notification_large_icon_height);
+ final int width =
+ (int) mContext.getResources().getDimension(android.R.dimen.notification_large_icon_width);
+ return BitmapUtil.getRoundedBitmap(bitmap, width, height);
+ }
+
+ /**
+ * Returns the appropriate icon res Id to display based on the call for which we want to display
+ * information.
+ */
+ private int getIconToDisplay(DialerCall call) {
+ // Even if both lines are in use, we only show a single item in
+ // the expanded Notifications UI. It's labeled "Ongoing call"
+ // (or "On hold" if there's only one call, and it's on hold.)
+ // Also, we don't have room to display caller-id info from two
+ // different calls. So if both lines are in use, display info
+ // from the foreground call. And if there's a ringing call,
+ // display that regardless of the state of the other calls.
+ if (call.getState() == DialerCall.State.ONHOLD) {
+ return R.drawable.ic_phone_paused_white_24dp;
+ } else if (call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ return R.drawable.ic_videocam;
+ }
+ return R.anim.on_going_call;
+ }
+
+ /** Returns the message to use with the notification. */
+ private String getContentString(DialerCall call, @UserType long userType) {
+ boolean isIncomingOrWaiting =
+ call.getState() == DialerCall.State.INCOMING
+ || call.getState() == DialerCall.State.CALL_WAITING;
+
+ if (isIncomingOrWaiting
+ && call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED) {
+
+ if (!TextUtils.isEmpty(call.getChildNumber())) {
+ return mContext.getString(R.string.child_number, call.getChildNumber());
+ } else if (!TextUtils.isEmpty(call.getCallSubject()) && call.isCallSubjectSupported()) {
+ return call.getCallSubject();
+ }
+ }
+
+ int resId = R.string.notification_ongoing_call;
+ if (call.hasProperty(Details.PROPERTY_WIFI)) {
+ resId = R.string.notification_ongoing_call_wifi;
+ }
+
+ if (isIncomingOrWaiting) {
+ if (call.hasProperty(Details.PROPERTY_WIFI)) {
+ resId = R.string.notification_incoming_call_wifi;
+ } else {
+ if (call.isSpam()) {
+ resId = R.string.notification_incoming_spam_call;
+ } else {
+ resId = R.string.notification_incoming_call;
+ }
+ }
+ } else if (call.getState() == DialerCall.State.ONHOLD) {
+ resId = R.string.notification_on_hold;
+ } else if (DialerCall.State.isDialing(call.getState())) {
+ resId = R.string.notification_dialing;
+ } else if (call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ resId = R.string.notification_requesting_video_call;
+ }
+
+ // Is the call placed through work connection service.
+ boolean isWorkCall = call.hasProperty(PROPERTY_ENTERPRISE_CALL);
+ if (userType == ContactsUtils.USER_TYPE_WORK || isWorkCall) {
+ resId = getWorkStringFromPersonalString(resId);
+ }
+
+ return mContext.getString(resId);
+ }
+
+ /** Gets the most relevant call to display in the notification. */
+ private DialerCall getCallToShow(CallList callList) {
+ if (callList == null) {
+ return null;
+ }
+ DialerCall call = callList.getIncomingCall();
+ if (call == null) {
+ call = callList.getOutgoingCall();
+ }
+ if (call == null) {
+ call = callList.getVideoUpgradeRequestCall();
+ }
+ if (call == null) {
+ call = callList.getActiveOrBackgroundCall();
+ }
+ return call;
+ }
+
+ private Spannable getActionText(@StringRes int stringRes, @ColorRes int colorRes) {
+ Spannable spannable = new SpannableString(mContext.getText(stringRes));
+ if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
+ // This will only work for cases where the Notification.Builder has a fullscreen intent set
+ // Notification.Builder that does not have a full screen intent will take the color of the
+ // app and the following leads to a no-op.
+ spannable.setSpan(
+ new ForegroundColorSpan(mContext.getColor(colorRes)), 0, spannable.length(), 0);
+ }
+ return spannable;
+ }
+
+ private void addAnswerAction(Notification.Builder builder) {
+ Log.d(this, "Will show \"answer\" action in the incoming call Notification");
+ PendingIntent answerVoicePendingIntent =
+ createNotificationPendingIntent(mContext, ACTION_ANSWER_VOICE_INCOMING_CALL);
+ builder.addAction(
+ R.anim.on_going_call,
+ getActionText(R.string.notification_action_answer, R.color.notification_action_accept),
+ answerVoicePendingIntent);
+ }
+
+ private void addDismissAction(Notification.Builder builder) {
+ Log.d(this, "Will show \"decline\" action in the incoming call Notification");
+ PendingIntent declinePendingIntent =
+ createNotificationPendingIntent(mContext, ACTION_DECLINE_INCOMING_CALL);
+ builder.addAction(
+ R.drawable.ic_close_dk,
+ getActionText(R.string.notification_action_dismiss, R.color.notification_action_dismiss),
+ declinePendingIntent);
+ }
+
+ private void addHangupAction(Notification.Builder builder) {
+ Log.d(this, "Will show \"hang-up\" action in the ongoing active call Notification");
+ PendingIntent hangupPendingIntent =
+ createNotificationPendingIntent(mContext, ACTION_HANG_UP_ONGOING_CALL);
+ builder.addAction(
+ R.drawable.ic_call_end_white_24dp,
+ getActionText(R.string.notification_action_end_call, R.color.notification_action_end_call),
+ hangupPendingIntent);
+ }
+
+ private void addVideoCallAction(Notification.Builder builder) {
+ Log.i(this, "Will show \"video\" action in the incoming call Notification");
+ PendingIntent answerVideoPendingIntent =
+ createNotificationPendingIntent(mContext, ACTION_ANSWER_VIDEO_INCOMING_CALL);
+ builder.addAction(
+ R.drawable.ic_videocam,
+ getActionText(
+ R.string.notification_action_answer_video, R.color.notification_action_answer_video),
+ answerVideoPendingIntent);
+ }
+
+ private void addAcceptUpgradeRequestAction(Notification.Builder builder) {
+ Log.i(this, "Will show \"accept upgrade\" action in the incoming call Notification");
+ PendingIntent acceptVideoPendingIntent =
+ createNotificationPendingIntent(mContext, ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST);
+ builder.addAction(
+ R.drawable.ic_videocam,
+ getActionText(R.string.notification_action_accept, R.color.notification_action_accept),
+ acceptVideoPendingIntent);
+ }
+
+ private void addDismissUpgradeRequestAction(Notification.Builder builder) {
+ Log.i(this, "Will show \"dismiss upgrade\" action in the incoming call Notification");
+ PendingIntent declineVideoPendingIntent =
+ createNotificationPendingIntent(mContext, ACTION_DECLINE_VIDEO_UPGRADE_REQUEST);
+ builder.addAction(
+ R.drawable.ic_videocam,
+ getActionText(R.string.notification_action_dismiss, R.color.notification_action_dismiss),
+ declineVideoPendingIntent);
+ }
+
+ /** Adds fullscreen intent to the builder. */
+ private void configureFullScreenIntent(
+ Notification.Builder builder, PendingIntent intent, CallList callList, DialerCall call) {
+ // Ok, we actually want to launch the incoming call
+ // UI at this point (in addition to simply posting a notification
+ // to the status bar). Setting fullScreenIntent will cause
+ // the InCallScreen to be launched immediately *unless* the
+ // current foreground activity is marked as "immersive".
+ Log.d(this, "- Setting fullScreenIntent: " + intent);
+ builder.setFullScreenIntent(intent, true);
+
+ // Ugly hack alert:
+ //
+ // The NotificationManager has the (undocumented) behavior
+ // that it will *ignore* the fullScreenIntent field if you
+ // post a new Notification that matches the ID of one that's
+ // already active. Unfortunately this is exactly what happens
+ // when you get an incoming call-waiting call: the
+ // "ongoing call" notification is already visible, so the
+ // InCallScreen won't get launched in this case!
+ // (The result: if you bail out of the in-call UI while on a
+ // call and then get a call-waiting call, the incoming call UI
+ // won't come up automatically.)
+ //
+ // The workaround is to just notice this exact case (this is a
+ // call-waiting call *and* the InCallScreen is not in the
+ // foreground) and manually cancel the in-call notification
+ // before (re)posting it.
+ //
+ // TODO: there should be a cleaner way of avoiding this
+ // problem (see discussion in bug 3184149.)
+
+ // If a call is onhold during an incoming call, the call actually comes in as
+ // INCOMING. For that case *and* traditional call-waiting, we want to
+ // cancel the notification.
+ boolean isCallWaiting =
+ (call.getState() == DialerCall.State.CALL_WAITING
+ || (call.getState() == DialerCall.State.INCOMING
+ && callList.getBackgroundCall() != null));
+
+ if (isCallWaiting) {
+ Log.i(this, "updateInCallNotification: call-waiting! force relaunch...");
+ // Cancel the IN_CALL_NOTIFICATION immediately before
+ // (re)posting it; this seems to force the
+ // NotificationManager to launch the fullScreenIntent.
+ mNotificationManager.cancel(NOTIFICATION_IN_CALL);
+ }
+ }
+
+ private Notification.Builder getNotificationBuilder() {
+ final Notification.Builder builder = new Notification.Builder(mContext);
+ builder.setOngoing(true);
+
+ // Make the notification prioritized over the other normal notifications.
+ builder.setPriority(Notification.PRIORITY_HIGH);
+
+ return builder;
+ }
+
+ private PendingIntent createLaunchPendingIntent(boolean isFullScreen, boolean isVideoCall) {
+ Intent intent =
+ InCallActivity.getIntent(
+ mContext,
+ false /* showDialpad */,
+ false /* newOutgoingCall */,
+ isVideoCall,
+ isFullScreen);
+
+ int requestCode = PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN;
+ if (isFullScreen) {
+ // Use a unique request code so that the pending intent isn't clobbered by the
+ // non-full screen pending intent.
+ requestCode = PENDING_INTENT_REQUEST_CODE_FULL_SCREEN;
+ }
+
+ // PendingIntent that can be used to launch the InCallActivity. The
+ // system fires off this intent if the user pulls down the windowshade
+ // and clicks the notification's expanded view. It's also used to
+ // launch the InCallActivity immediately when when there's an incoming
+ // call (see the "fullScreenIntent" field below).
+ return PendingIntent.getActivity(mContext, requestCode, intent, 0);
+ }
+
+ private void setStatusBarCallListener(StatusBarCallListener listener) {
+ if (mStatusBarCallListener != null) {
+ mStatusBarCallListener.cleanup();
+ }
+ mStatusBarCallListener = listener;
+ }
+
+ private class StatusBarCallListener implements DialerCallListener {
+
+ private DialerCall mDialerCall;
+
+ StatusBarCallListener(DialerCall dialerCall) {
+ mDialerCall = dialerCall;
+ mDialerCall.addListener(this);
+ }
+
+ void cleanup() {
+ mDialerCall.removeListener(this);
+ }
+
+ @Override
+ public void onDialerCallDisconnect() {}
+
+ @Override
+ public void onDialerCallUpdate() {
+ if (CallList.getInstance().getIncomingCall() == null) {
+ mDialerRingtoneManager.stopCallWaitingTone();
+ }
+ }
+
+ @Override
+ public void onDialerCallChildNumberChange() {}
+
+ @Override
+ public void onDialerCallLastForwardedNumberChange() {}
+
+ @Override
+ public void onDialerCallUpgradeToVideo() {}
+
+ @Override
+ public void onWiFiToLteHandover() {}
+
+ @Override
+ public void onHandoverToWifiFailure() {}
+
+ /**
+ * Responds to changes in the session modification state for the call by dismissing the status
+ * bar notification as required.
+ */
+ @Override
+ public void onDialerCallSessionModificationStateChange(@SessionModificationState int state) {
+ if (state == DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST) {
+ cleanup();
+ updateNotification(CallList.getInstance());
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/ThemeColorManager.java b/java/com/android/incallui/ThemeColorManager.java
new file mode 100644
index 000000000..a88ae33cd
--- /dev/null
+++ b/java/com/android/incallui/ThemeColorManager.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.support.annotation.ColorInt;
+import android.support.annotation.Nullable;
+import android.support.v4.graphics.ColorUtils;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import com.android.contacts.common.util.MaterialColorMapUtils;
+import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette;
+import com.android.incallui.call.DialerCall;
+
+/**
+ * Calculates the background color for the in call window. The background color is based on the SIM
+ * and spam status.
+ */
+public class ThemeColorManager {
+ private final MaterialColorMapUtils colorMap;
+ @ColorInt private int primaryColor;
+ @ColorInt private int secondaryColor;
+ @ColorInt private int backgroundColorTop;
+ @ColorInt private int backgroundColorMiddle;
+ @ColorInt private int backgroundColorBottom;
+ @ColorInt private int backgroundColorSolid;
+
+ /**
+ * If there is no actual call currently in the call list, this will be used as a fallback to
+ * determine the theme color for InCallUI.
+ */
+ @Nullable private PhoneAccountHandle pendingPhoneAccountHandle;
+
+ public ThemeColorManager(MaterialColorMapUtils colorMap) {
+ this.colorMap = colorMap;
+ }
+
+ public void setPendingPhoneAccountHandle(@Nullable PhoneAccountHandle pendingPhoneAccountHandle) {
+ this.pendingPhoneAccountHandle = pendingPhoneAccountHandle;
+ }
+
+ public void onForegroundCallChanged(Context context, @Nullable DialerCall newForegroundCall) {
+ if (newForegroundCall == null) {
+ updateThemeColors(context, pendingPhoneAccountHandle, false);
+ } else {
+ updateThemeColors(context, newForegroundCall.getAccountHandle(), newForegroundCall.isSpam());
+ }
+ }
+
+ private void updateThemeColors(
+ Context context, @Nullable PhoneAccountHandle handle, boolean isSpam) {
+ MaterialPalette palette;
+ if (isSpam) {
+ palette =
+ colorMap.calculatePrimaryAndSecondaryColor(R.color.incall_call_spam_background_color);
+ backgroundColorTop = context.getColor(R.color.incall_background_gradient_spam_top);
+ backgroundColorMiddle = context.getColor(R.color.incall_background_gradient_spam_middle);
+ backgroundColorBottom = context.getColor(R.color.incall_background_gradient_spam_bottom);
+ backgroundColorSolid = context.getColor(R.color.incall_background_multiwindow_spam);
+ } else {
+ @ColorInt int highlightColor = getHighlightColor(context, handle);
+ palette = colorMap.calculatePrimaryAndSecondaryColor(highlightColor);
+ backgroundColorTop = context.getColor(R.color.incall_background_gradient_top);
+ backgroundColorMiddle = context.getColor(R.color.incall_background_gradient_middle);
+ backgroundColorBottom = context.getColor(R.color.incall_background_gradient_bottom);
+ backgroundColorSolid = context.getColor(R.color.incall_background_multiwindow);
+ if (highlightColor != PhoneAccount.NO_HIGHLIGHT_COLOR) {
+ // The default background gradient has a subtle alpha. We grab that alpha and apply it to
+ // the phone account color.
+ backgroundColorTop = applyAlpha(palette.mPrimaryColor, backgroundColorTop);
+ backgroundColorMiddle = applyAlpha(palette.mPrimaryColor, backgroundColorMiddle);
+ backgroundColorBottom = applyAlpha(palette.mPrimaryColor, backgroundColorBottom);
+ backgroundColorSolid = applyAlpha(palette.mPrimaryColor, backgroundColorSolid);
+ }
+ }
+
+ primaryColor = palette.mPrimaryColor;
+ secondaryColor = palette.mSecondaryColor;
+ }
+
+ @ColorInt
+ private static int getHighlightColor(Context context, @Nullable PhoneAccountHandle handle) {
+ if (handle != null) {
+ PhoneAccount account = context.getSystemService(TelecomManager.class).getPhoneAccount(handle);
+ if (account != null) {
+ return account.getHighlightColor();
+ }
+ }
+ return PhoneAccount.NO_HIGHLIGHT_COLOR;
+ }
+
+ @ColorInt
+ public int getPrimaryColor() {
+ return primaryColor;
+ }
+
+ @ColorInt
+ public int getSecondaryColor() {
+ return secondaryColor;
+ }
+
+ @ColorInt
+ public int getBackgroundColorTop() {
+ return backgroundColorTop;
+ }
+
+ @ColorInt
+ public int getBackgroundColorMiddle() {
+ return backgroundColorMiddle;
+ }
+
+ @ColorInt
+ public int getBackgroundColorBottom() {
+ return backgroundColorBottom;
+ }
+
+ @ColorInt
+ public int getBackgroundColorSolid() {
+ return backgroundColorSolid;
+ }
+
+ @ColorInt
+ private static int applyAlpha(@ColorInt int color, @ColorInt int sourceColorWithAlpha) {
+ return ColorUtils.setAlphaComponent(color, Color.alpha(sourceColorWithAlpha));
+ }
+}
diff --git a/java/com/android/incallui/TransactionSafeFragmentActivity.java b/java/com/android/incallui/TransactionSafeFragmentActivity.java
new file mode 100644
index 000000000..a6b078cb4
--- /dev/null
+++ b/java/com/android/incallui/TransactionSafeFragmentActivity.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui;
+
+import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
+
+/**
+ * A common superclass that keeps track of whether an {@link Activity} has saved its state yet or
+ * not.
+ */
+public abstract class TransactionSafeFragmentActivity extends FragmentActivity {
+
+ private boolean mIsSafeToCommitTransactions;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mIsSafeToCommitTransactions = true;
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mIsSafeToCommitTransactions = true;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mIsSafeToCommitTransactions = true;
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mIsSafeToCommitTransactions = false;
+ }
+
+ /**
+ * Returns true if it is safe to commit {@link FragmentTransaction}s at this time, based on
+ * whether {@link Activity#onSaveInstanceState} has been called or not.
+ *
+ * <p>Make sure that the current activity calls into {@link super.onSaveInstanceState(Bundle
+ * outState)} (if that method is overridden), so the flag is properly set.
+ */
+ public boolean isSafeToCommitTransactions() {
+ return mIsSafeToCommitTransactions;
+ }
+}
diff --git a/java/com/android/incallui/VideoCallPresenter.java b/java/com/android/incallui/VideoCallPresenter.java
new file mode 100644
index 000000000..971b6957a
--- /dev/null
+++ b/java/com/android/incallui/VideoCallPresenter.java
@@ -0,0 +1,1289 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Point;
+import android.os.Handler;
+import android.support.annotation.Nullable;
+import android.telecom.Connection;
+import android.telecom.InCallService.VideoCall;
+import android.telecom.VideoProfile;
+import android.telecom.VideoProfile.CameraCapabilities;
+import android.view.Surface;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.CompatUtils;
+import com.android.incallui.InCallPresenter.InCallDetailsListener;
+import com.android.incallui.InCallPresenter.InCallOrientationListener;
+import com.android.incallui.InCallPresenter.InCallStateListener;
+import com.android.incallui.InCallPresenter.IncomingCallListener;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.call.DialerCall.State;
+import com.android.incallui.call.InCallVideoCallCallbackNotifier;
+import com.android.incallui.call.InCallVideoCallCallbackNotifier.SurfaceChangeListener;
+import com.android.incallui.call.InCallVideoCallCallbackNotifier.VideoEventListener;
+import com.android.incallui.call.VideoUtils;
+import com.android.incallui.util.AccessibilityUtil;
+import com.android.incallui.video.protocol.VideoCallScreen;
+import com.android.incallui.video.protocol.VideoCallScreenDelegate;
+import com.android.incallui.videosurface.protocol.VideoSurfaceDelegate;
+import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
+import java.util.Objects;
+
+/**
+ * Logic related to the {@link VideoCallScreen} and for managing changes to the video calling
+ * surfaces based on other user interface events and incoming events from the {@class
+ * VideoCallListener}.
+ *
+ * <p>When a call's video state changes to bi-directional video, the {@link
+ * com.android.incallui.VideoCallPresenter} performs the following negotiation with the telephony
+ * layer:
+ *
+ * <ul>
+ * <li>{@code VideoCallPresenter} creates and informs telephony of the display surface.
+ * <li>{@code VideoCallPresenter} creates the preview surface.
+ * <li>{@code VideoCallPresenter} informs telephony of the currently selected camera.
+ * <li>Telephony layer sends {@link CameraCapabilities}, including the dimensions of the video for
+ * the current camera.
+ * <li>{@code VideoCallPresenter} adjusts size of the preview surface to match the aspect ratio of
+ * the camera.
+ * <li>{@code VideoCallPresenter} informs telephony of the new preview surface.
+ * </ul>
+ *
+ * <p>When downgrading to an audio-only video state, the {@code VideoCallPresenter} nulls both
+ * surfaces.
+ */
+public class VideoCallPresenter
+ implements IncomingCallListener,
+ InCallOrientationListener,
+ InCallStateListener,
+ InCallDetailsListener,
+ SurfaceChangeListener,
+ VideoEventListener,
+ InCallPresenter.InCallEventListener,
+ VideoCallScreenDelegate {
+
+ private static boolean mIsVideoMode = false;
+
+ private final Handler mHandler = new Handler();
+ private VideoCallScreen mVideoCallScreen;
+
+ /** The current context. */
+ private Context mContext;
+
+ @Override
+ public boolean shouldShowCameraPermissionDialog() {
+ if (mPrimaryCall == null) {
+ LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "null call");
+ return false;
+ }
+ if (mPrimaryCall.didShowCameraPermission()) {
+ LogUtil.i(
+ "VideoCallPresenter.shouldShowCameraPermissionDialog", "already shown for this call");
+ return false;
+ }
+ if (!ConfigProviderBindings.get(mContext)
+ .getBoolean("camera_permission_dialog_allowed", true)) {
+ LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "disabled by config");
+ return false;
+ }
+ return !VideoUtils.hasCameraPermission(mContext) || !VideoUtils.isCameraAllowedByUser(mContext);
+ }
+
+ @Override
+ public void onCameraPermissionDialogShown() {
+ if (mPrimaryCall != null) {
+ mPrimaryCall.setDidShowCameraPermission(true);
+ }
+ }
+
+ /** The call the video surfaces are currently related to */
+ private DialerCall mPrimaryCall;
+ /**
+ * The {@link VideoCall} used to inform the video telephony layer of changes to the video
+ * surfaces.
+ */
+ private VideoCall mVideoCall;
+ /** Determines if the current UI state represents a video call. */
+ private int mCurrentVideoState;
+ /** DialerCall's current state */
+ private int mCurrentCallState = DialerCall.State.INVALID;
+ /** Determines the device orientation (portrait/lanscape). */
+ private int mDeviceOrientation = InCallOrientationEventListener.SCREEN_ORIENTATION_UNKNOWN;
+ /** Tracks the state of the preview surface negotiation with the telephony layer. */
+ private int mPreviewSurfaceState = PreviewSurfaceState.NONE;
+ /**
+ * Determines whether video calls should automatically enter full screen mode after {@link
+ * #mAutoFullscreenTimeoutMillis} milliseconds.
+ */
+ private boolean mIsAutoFullscreenEnabled = false;
+ /**
+ * Determines the number of milliseconds after which a video call will automatically enter
+ * fullscreen mode. Requires {@link #mIsAutoFullscreenEnabled} to be {@code true}.
+ */
+ private int mAutoFullscreenTimeoutMillis = 0;
+ /**
+ * Determines if the countdown is currently running to automatically enter full screen video mode.
+ */
+ private boolean mAutoFullScreenPending = false;
+ /** Whether if the call is remotely held. */
+ private boolean mIsRemotelyHeld = false;
+ /**
+ * Runnable which is posted to schedule automatically entering fullscreen mode. Will not auto
+ * enter fullscreen mode if the dialpad is visible (doing so would make it impossible to exit the
+ * dialpad).
+ */
+ private Runnable mAutoFullscreenRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ if (mAutoFullScreenPending
+ && !InCallPresenter.getInstance().isDialpadVisible()
+ && mIsVideoMode) {
+
+ LogUtil.v("VideoCallPresenter.mAutoFullScreenRunnable", "entering fullscreen mode");
+ InCallPresenter.getInstance().setFullScreen(true);
+ mAutoFullScreenPending = false;
+ } else {
+ LogUtil.v(
+ "VideoCallPresenter.mAutoFullScreenRunnable",
+ "skipping scheduled fullscreen mode.");
+ }
+ }
+ };
+
+ private boolean isVideoCallScreenUiReady;
+
+ private static boolean isCameraRequired(int videoState, int sessionModificationState) {
+ return VideoProfile.isBidirectional(videoState)
+ || VideoProfile.isTransmissionEnabled(videoState)
+ || isVideoUpgrade(sessionModificationState);
+ }
+
+ /**
+ * Determines if the incoming video surface should be shown based on the current videoState and
+ * callState. The video surface is shown when incoming video is not paused, the call is active,
+ * and video reception is enabled.
+ *
+ * @param videoState The current video state.
+ * @param callState The current call state.
+ * @return {@code true} if the incoming video surface should be shown, {@code false} otherwise.
+ */
+ public static boolean showIncomingVideo(int videoState, int callState) {
+ if (!CompatUtils.isVideoCompatible()) {
+ return false;
+ }
+
+ boolean isPaused = VideoProfile.isPaused(videoState);
+ boolean isCallActive = callState == DialerCall.State.ACTIVE;
+
+ return !isPaused && isCallActive && VideoProfile.isReceptionEnabled(videoState);
+ }
+
+ /**
+ * Determines if the outgoing video surface should be shown based on the current videoState. The
+ * video surface is shown if video transmission is enabled.
+ *
+ * @return {@code true} if the the outgoing video surface should be shown, {@code false}
+ * otherwise.
+ */
+ public static boolean showOutgoingVideo(
+ Context context, int videoState, int sessionModificationState) {
+ if (!VideoUtils.hasCameraPermissionAndAllowedByUser(context)) {
+ LogUtil.i("VideoCallPresenter.showOutgoingVideo", "Camera permission is disabled by user.");
+ return false;
+ }
+
+ if (!CompatUtils.isVideoCompatible()) {
+ return false;
+ }
+
+ return VideoProfile.isTransmissionEnabled(videoState)
+ || isVideoUpgrade(sessionModificationState);
+ }
+
+ private static void updateCameraSelection(DialerCall call) {
+ LogUtil.v("VideoCallPresenter.updateCameraSelection", "call=" + call);
+ LogUtil.v("VideoCallPresenter.updateCameraSelection", "call=" + toSimpleString(call));
+
+ final DialerCall activeCall = CallList.getInstance().getActiveCall();
+ int cameraDir;
+
+ // this function should never be called with null call object, however if it happens we
+ // should handle it gracefully.
+ if (call == null) {
+ cameraDir = DialerCall.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
+ LogUtil.e(
+ "VideoCallPresenter.updateCameraSelection",
+ "call is null. Setting camera direction to default value (CAMERA_DIRECTION_UNKNOWN)");
+ }
+
+ // Clear camera direction if this is not a video call.
+ else if (VideoUtils.isAudioCall(call) && !isVideoUpgrade(call)) {
+ cameraDir = DialerCall.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
+ call.getVideoSettings().setCameraDir(cameraDir);
+ }
+
+ // If this is a waiting video call, default to active call's camera,
+ // since we don't want to change the current camera for waiting call
+ // without user's permission.
+ else if (VideoUtils.isVideoCall(activeCall) && VideoUtils.isIncomingVideoCall(call)) {
+ cameraDir = activeCall.getVideoSettings().getCameraDir();
+ }
+
+ // Infer the camera direction from the video state and store it,
+ // if this is an outgoing video call.
+ else if (VideoUtils.isOutgoingVideoCall(call) && !isCameraDirectionSet(call)) {
+ cameraDir = toCameraDirection(call.getVideoState());
+ call.getVideoSettings().setCameraDir(cameraDir);
+ }
+
+ // Use the stored camera dir if this is an outgoing video call for which camera direction
+ // is set.
+ else if (VideoUtils.isOutgoingVideoCall(call)) {
+ cameraDir = call.getVideoSettings().getCameraDir();
+ }
+
+ // Infer the camera direction from the video state and store it,
+ // if this is an active video call and camera direction is not set.
+ else if (VideoUtils.isActiveVideoCall(call) && !isCameraDirectionSet(call)) {
+ cameraDir = toCameraDirection(call.getVideoState());
+ call.getVideoSettings().setCameraDir(cameraDir);
+ }
+
+ // Use the stored camera dir if this is an active video call for which camera direction
+ // is set.
+ else if (VideoUtils.isActiveVideoCall(call)) {
+ cameraDir = call.getVideoSettings().getCameraDir();
+ }
+
+ // For all other cases infer the camera direction but don't store it in the call object.
+ else {
+ cameraDir = toCameraDirection(call.getVideoState());
+ }
+
+ LogUtil.i(
+ "VideoCallPresenter.updateCameraSelection",
+ "setting camera direction to %d, call: %s",
+ cameraDir,
+ call);
+ final InCallCameraManager cameraManager =
+ InCallPresenter.getInstance().getInCallCameraManager();
+ cameraManager.setUseFrontFacingCamera(
+ cameraDir == DialerCall.VideoSettings.CAMERA_DIRECTION_FRONT_FACING);
+ }
+
+ private static int toCameraDirection(int videoState) {
+ return VideoProfile.isTransmissionEnabled(videoState)
+ && !VideoProfile.isBidirectional(videoState)
+ ? DialerCall.VideoSettings.CAMERA_DIRECTION_BACK_FACING
+ : DialerCall.VideoSettings.CAMERA_DIRECTION_FRONT_FACING;
+ }
+
+ private static boolean isCameraDirectionSet(DialerCall call) {
+ return VideoUtils.isVideoCall(call)
+ && call.getVideoSettings().getCameraDir()
+ != DialerCall.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
+ }
+
+ private static String toSimpleString(DialerCall call) {
+ return call == null ? null : call.toSimpleString();
+ }
+
+ /**
+ * Initializes the presenter.
+ *
+ * @param context The current context.
+ */
+ @Override
+ public void initVideoCallScreenDelegate(Context context, VideoCallScreen videoCallScreen) {
+ mContext = context;
+ mVideoCallScreen = videoCallScreen;
+ mIsAutoFullscreenEnabled =
+ mContext.getResources().getBoolean(R.bool.video_call_auto_fullscreen);
+ mAutoFullscreenTimeoutMillis =
+ mContext.getResources().getInteger(R.integer.video_call_auto_fullscreen_timeout);
+ }
+
+ /** Called when the user interface is ready to be used. */
+ @Override
+ public void onVideoCallScreenUiReady() {
+ LogUtil.v("VideoCallPresenter.onVideoCallScreenUiReady", "");
+ Assert.checkState(!isVideoCallScreenUiReady);
+
+ // Do not register any listeners if video calling is not compatible to safeguard against
+ // any accidental calls of video calling code.
+ if (!CompatUtils.isVideoCompatible()) {
+ return;
+ }
+
+ mDeviceOrientation = InCallOrientationEventListener.getCurrentOrientation();
+
+ // Register for call state changes last
+ InCallPresenter.getInstance().addListener(this);
+ InCallPresenter.getInstance().addDetailsListener(this);
+ InCallPresenter.getInstance().addIncomingCallListener(this);
+ InCallPresenter.getInstance().addOrientationListener(this);
+ // To get updates of video call details changes
+ InCallPresenter.getInstance().addInCallEventListener(this);
+ InCallPresenter.getInstance().getLocalVideoSurfaceTexture().setDelegate(new LocalDelegate());
+ InCallPresenter.getInstance().getRemoteVideoSurfaceTexture().setDelegate(new RemoteDelegate());
+
+ // Register for surface and video events from {@link InCallVideoCallListener}s.
+ InCallVideoCallCallbackNotifier.getInstance().addSurfaceChangeListener(this);
+ InCallVideoCallCallbackNotifier.getInstance().addVideoEventListener(this);
+ mCurrentVideoState = VideoProfile.STATE_AUDIO_ONLY;
+ mCurrentCallState = DialerCall.State.INVALID;
+
+ InCallPresenter.InCallState inCallState = InCallPresenter.getInstance().getInCallState();
+ onStateChange(inCallState, inCallState, CallList.getInstance());
+ isVideoCallScreenUiReady = true;
+ }
+
+ /** Called when the user interface is no longer ready to be used. */
+ @Override
+ public void onVideoCallScreenUiUnready() {
+ LogUtil.v("VideoCallPresenter.onVideoCallScreenUiUnready", "");
+ Assert.checkState(isVideoCallScreenUiReady);
+
+ if (!CompatUtils.isVideoCompatible()) {
+ return;
+ }
+
+ cancelAutoFullScreen();
+
+ InCallPresenter.getInstance().removeListener(this);
+ InCallPresenter.getInstance().removeDetailsListener(this);
+ InCallPresenter.getInstance().removeIncomingCallListener(this);
+ InCallPresenter.getInstance().removeOrientationListener(this);
+ InCallPresenter.getInstance().removeInCallEventListener(this);
+ InCallPresenter.getInstance().getLocalVideoSurfaceTexture().setDelegate(null);
+
+ InCallVideoCallCallbackNotifier.getInstance().removeSurfaceChangeListener(this);
+ InCallVideoCallCallbackNotifier.getInstance().removeVideoEventListener(this);
+
+ // Ensure that the call's camera direction is updated (most likely to UNKNOWN). Normally this
+ // happens after any call state changes but we're unregistering from InCallPresenter above so
+ // we won't get any more call state changes. See b/32957114.
+ if (mPrimaryCall != null) {
+ updateCameraSelection(mPrimaryCall);
+ }
+
+ isVideoCallScreenUiReady = false;
+ }
+
+ /**
+ * Handles clicks on the video surfaces. If not currently in fullscreen mode, will set fullscreen.
+ */
+ private void onSurfaceClick() {
+ LogUtil.i("VideoCallPresenter.onSurfaceClick", "");
+ cancelAutoFullScreen();
+ if (!InCallPresenter.getInstance().isFullscreen()) {
+ InCallPresenter.getInstance().setFullScreen(true);
+ } else {
+ InCallPresenter.getInstance().setFullScreen(false);
+ maybeAutoEnterFullscreen(mPrimaryCall);
+ // If Activity is not multiwindow, fullscreen will be driven by SystemUI visibility changes
+ // instead. See #onSystemUiVisibilityChange(boolean)
+
+ // TODO (keyboardr): onSystemUiVisibilityChange isn't being called the first time
+ // visibility changes after orientation change, so this is currently always done as a backup.
+ }
+ }
+
+ @Override
+ public void onSystemUiVisibilityChange(boolean visible) {
+ // If the SystemUI has changed to be visible, take us out of fullscreen mode
+ LogUtil.i("VideoCallPresenter.onSystemUiVisibilityChange", "visible: " + visible);
+ if (visible) {
+ InCallPresenter.getInstance().setFullScreen(false);
+ maybeAutoEnterFullscreen(mPrimaryCall);
+ }
+ }
+
+ @Override
+ public VideoSurfaceTexture getLocalVideoSurfaceTexture() {
+ return InCallPresenter.getInstance().getLocalVideoSurfaceTexture();
+ }
+
+ @Override
+ public VideoSurfaceTexture getRemoteVideoSurfaceTexture() {
+ return InCallPresenter.getInstance().getRemoteVideoSurfaceTexture();
+ }
+
+ @Override
+ public int getDeviceOrientation() {
+ return mDeviceOrientation;
+ }
+
+ /**
+ * This should only be called when user approved the camera permission, which is local action and
+ * does NOT change any call states.
+ */
+ @Override
+ public void onCameraPermissionGranted() {
+ LogUtil.i("VideoCallPresenter.onCameraPermissionGranted", "");
+ VideoUtils.setCameraAllowedByUser(mContext);
+ enableCamera(mPrimaryCall.getVideoCall(), isCameraRequired());
+ showVideoUi(
+ mPrimaryCall.getVideoState(),
+ mPrimaryCall.getState(),
+ mPrimaryCall.getSessionModificationState(),
+ mPrimaryCall.isRemotelyHeld());
+ InCallPresenter.getInstance().getInCallCameraManager().onCameraPermissionGranted();
+ }
+
+ /**
+ * Called when the user interacts with the UI. If a fullscreen timer is pending then we start the
+ * timer from scratch to avoid having the UI disappear while the user is interacting with it.
+ */
+ @Override
+ public void resetAutoFullscreenTimer() {
+ if (mAutoFullScreenPending) {
+ LogUtil.i("VideoCallPresenter.resetAutoFullscreenTimer", "resetting");
+ mHandler.removeCallbacks(mAutoFullscreenRunnable);
+ mHandler.postDelayed(mAutoFullscreenRunnable, mAutoFullscreenTimeoutMillis);
+ }
+ }
+
+ /**
+ * Handles incoming calls.
+ *
+ * @param oldState The old in call state.
+ * @param newState The new in call state.
+ * @param call The call.
+ */
+ @Override
+ public void onIncomingCall(
+ InCallPresenter.InCallState oldState, InCallPresenter.InCallState newState, DialerCall call) {
+ // same logic should happen as with onStateChange()
+ onStateChange(oldState, newState, CallList.getInstance());
+ }
+
+ /**
+ * Handles state changes (including incoming calls)
+ *
+ * @param newState The in call state.
+ * @param callList The call list.
+ */
+ @Override
+ public void onStateChange(
+ InCallPresenter.InCallState oldState,
+ InCallPresenter.InCallState newState,
+ CallList callList) {
+ LogUtil.v(
+ "VideoCallPresenter.onStateChange",
+ "oldState: %s, newState: %s, isVideoMode: %b",
+ oldState,
+ newState,
+ isVideoMode());
+
+ if (newState == InCallPresenter.InCallState.NO_CALLS) {
+ if (isVideoMode()) {
+ exitVideoMode();
+ }
+
+ InCallPresenter.getInstance().cleanupSurfaces();
+ }
+
+ // Determine the primary active call).
+ DialerCall primary = null;
+
+ // Determine the call which is the focus of the user's attention. In the case of an
+ // incoming call waiting call, the primary call is still the active video call, however
+ // the determination of whether we should be in fullscreen mode is based on the type of the
+ // incoming call, not the active video call.
+ DialerCall currentCall = null;
+
+ if (newState == InCallPresenter.InCallState.INCOMING) {
+ // We don't want to replace active video call (primary call)
+ // with a waiting call, since user may choose to ignore/decline the waiting call and
+ // this should have no impact on current active video call, that is, we should not
+ // change the camera or UI unless the waiting VT call becomes active.
+ primary = callList.getActiveCall();
+ currentCall = callList.getIncomingCall();
+ if (!VideoUtils.isActiveVideoCall(primary)) {
+ primary = callList.getIncomingCall();
+ }
+ } else if (newState == InCallPresenter.InCallState.OUTGOING) {
+ currentCall = primary = callList.getOutgoingCall();
+ } else if (newState == InCallPresenter.InCallState.PENDING_OUTGOING) {
+ currentCall = primary = callList.getPendingOutgoingCall();
+ } else if (newState == InCallPresenter.InCallState.INCALL) {
+ currentCall = primary = callList.getActiveCall();
+ }
+
+ final boolean primaryChanged = !Objects.equals(mPrimaryCall, primary);
+ LogUtil.i(
+ "VideoCallPresenter.onStateChange",
+ "primaryChanged: %b, primary: %s, mPrimaryCall: %s",
+ primaryChanged,
+ primary,
+ mPrimaryCall);
+ if (primaryChanged) {
+ onPrimaryCallChanged(primary);
+ } else if (mPrimaryCall != null) {
+ updateVideoCall(primary);
+ }
+ updateCallCache(primary);
+
+ // If the call context changed, potentially exit fullscreen or schedule auto enter of
+ // fullscreen mode.
+ // If the current call context is no longer a video call, exit fullscreen mode.
+ maybeExitFullscreen(currentCall);
+ // Schedule auto-enter of fullscreen mode if the current call context is a video call
+ maybeAutoEnterFullscreen(currentCall);
+ }
+
+ /**
+ * Handles a change to the fullscreen mode of the app.
+ *
+ * @param isFullscreenMode {@code true} if the app is now fullscreen, {@code false} otherwise.
+ */
+ @Override
+ public void onFullscreenModeChanged(boolean isFullscreenMode) {
+ cancelAutoFullScreen();
+ if (mPrimaryCall != null) {
+ updateFullscreenAndGreenScreenMode(
+ mPrimaryCall.getState(), mPrimaryCall.getSessionModificationState());
+ } else {
+ updateFullscreenAndGreenScreenMode(
+ State.INVALID, DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+ }
+
+ private void checkForVideoStateChange(DialerCall call) {
+ final boolean shouldShowVideoUi = shouldShowVideoUiForCall(call);
+ final boolean hasVideoStateChanged = mCurrentVideoState != call.getVideoState();
+
+ LogUtil.v(
+ "VideoCallPresenter.checkForVideoStateChange",
+ "shouldShowVideoUi: %b, hasVideoStateChanged: %b, isVideoMode: %b, previousVideoState: %s,"
+ + " newVideoState: %s",
+ shouldShowVideoUi,
+ hasVideoStateChanged,
+ isVideoMode(),
+ VideoProfile.videoStateToString(mCurrentVideoState),
+ VideoProfile.videoStateToString(call.getVideoState()));
+ if (!hasVideoStateChanged) {
+ return;
+ }
+
+ updateCameraSelection(call);
+
+ if (shouldShowVideoUi) {
+ adjustVideoMode(call);
+ } else if (isVideoMode()) {
+ exitVideoMode();
+ }
+ }
+
+ private void checkForCallStateChange(DialerCall call) {
+ final boolean shouldShowVideoUi = shouldShowVideoUiForCall(call);
+ final boolean hasCallStateChanged =
+ mCurrentCallState != call.getState() || mIsRemotelyHeld != call.isRemotelyHeld();
+ mIsRemotelyHeld = call.isRemotelyHeld();
+
+ LogUtil.v(
+ "VideoCallPresenter.checkForCallStateChange",
+ "shouldShowVideoUi: %b, hasCallStateChanged: %b, isVideoMode: %b",
+ shouldShowVideoUi,
+ hasCallStateChanged,
+ isVideoMode());
+
+ if (!hasCallStateChanged) {
+ return;
+ }
+
+ if (shouldShowVideoUi) {
+ final InCallCameraManager cameraManager =
+ InCallPresenter.getInstance().getInCallCameraManager();
+
+ String prevCameraId = cameraManager.getActiveCameraId();
+ updateCameraSelection(call);
+ String newCameraId = cameraManager.getActiveCameraId();
+
+ if (!Objects.equals(prevCameraId, newCameraId) && VideoUtils.isActiveVideoCall(call)) {
+ enableCamera(call.getVideoCall(), true);
+ }
+ }
+
+ // Make sure we hide or show the video UI if needed.
+ showVideoUi(
+ call.getVideoState(),
+ call.getState(),
+ call.getSessionModificationState(),
+ call.isRemotelyHeld());
+ }
+
+ private void onPrimaryCallChanged(DialerCall newPrimaryCall) {
+ final boolean shouldShowVideoUi = shouldShowVideoUiForCall(newPrimaryCall);
+ final boolean isVideoMode = isVideoMode();
+
+ LogUtil.v(
+ "VideoCallPresenter.onPrimaryCallChanged",
+ "shouldShowVideoUi: %b, isVideoMode: %b",
+ shouldShowVideoUi,
+ isVideoMode);
+
+ if (!shouldShowVideoUi && isVideoMode) {
+ // Terminate video mode if new primary call is not a video call
+ // and we are currently in video mode.
+ LogUtil.i("VideoCallPresenter.onPrimaryCallChanged", "exiting video mode...");
+ exitVideoMode();
+ } else if (shouldShowVideoUi) {
+ LogUtil.i("VideoCallPresenter.onPrimaryCallChanged", "entering video mode...");
+
+ updateCameraSelection(newPrimaryCall);
+ adjustVideoMode(newPrimaryCall);
+ }
+ checkForOrientationAllowedChange(newPrimaryCall);
+ }
+
+ private boolean isVideoMode() {
+ return mIsVideoMode;
+ }
+
+ private void updateCallCache(DialerCall call) {
+ if (call == null) {
+ mCurrentVideoState = VideoProfile.STATE_AUDIO_ONLY;
+ mCurrentCallState = DialerCall.State.INVALID;
+ mVideoCall = null;
+ mPrimaryCall = null;
+ } else {
+ mCurrentVideoState = call.getVideoState();
+ mVideoCall = call.getVideoCall();
+ mCurrentCallState = call.getState();
+ mPrimaryCall = call;
+ }
+ }
+
+ /**
+ * Handles changes to the details of the call. The {@link VideoCallPresenter} is interested in
+ * changes to the video state.
+ *
+ * @param call The call for which the details changed.
+ * @param details The new call details.
+ */
+ @Override
+ public void onDetailsChanged(DialerCall call, android.telecom.Call.Details details) {
+ LogUtil.v(
+ "VideoCallPresenter.onDetailsChanged",
+ "call: %s, details: %s, mPrimaryCall: %s",
+ call,
+ details,
+ mPrimaryCall);
+ if (call == null) {
+ return;
+ }
+ // If the details change is not for the currently active call no update is required.
+ if (!call.equals(mPrimaryCall)) {
+ LogUtil.v("VideoCallPresenter.onDetailsChanged", "details not for current active call");
+ return;
+ }
+
+ updateVideoCall(call);
+
+ updateCallCache(call);
+ }
+
+ private void updateVideoCall(DialerCall call) {
+ checkForVideoCallChange(call);
+ checkForVideoStateChange(call);
+ checkForCallStateChange(call);
+ checkForOrientationAllowedChange(call);
+ updateFullscreenAndGreenScreenMode(call.getState(), call.getSessionModificationState());
+ }
+
+ private void checkForOrientationAllowedChange(@Nullable DialerCall call) {
+ InCallPresenter.getInstance()
+ .setInCallAllowsOrientationChange(VideoUtils.isVideoCall(call) || isVideoUpgrade(call));
+ }
+
+ private void updateFullscreenAndGreenScreenMode(
+ int callState, @SessionModificationState int sessionModificationState) {
+ if (mVideoCallScreen != null) {
+ boolean shouldShowFullscreen = InCallPresenter.getInstance().isFullscreen();
+ boolean shouldShowGreenScreen =
+ callState == State.DIALING
+ || callState == State.CONNECTING
+ || callState == State.INCOMING
+ || isVideoUpgrade(sessionModificationState);
+ mVideoCallScreen.updateFullscreenAndGreenScreenMode(
+ shouldShowFullscreen, shouldShowGreenScreen);
+ }
+ }
+
+ /** Checks for a change to the video call and changes it if required. */
+ private void checkForVideoCallChange(DialerCall call) {
+ final VideoCall videoCall = call.getVideoCall();
+ LogUtil.v(
+ "VideoCallPresenter.checkForVideoCallChange",
+ "videoCall: %s, mVideoCall: %s",
+ videoCall,
+ mVideoCall);
+ if (!Objects.equals(videoCall, mVideoCall)) {
+ changeVideoCall(call);
+ }
+ }
+
+ /**
+ * Handles a change to the video call. Sets the surfaces on the previous call to null and sets the
+ * surfaces on the new video call accordingly.
+ *
+ * @param call The new video call.
+ */
+ private void changeVideoCall(DialerCall call) {
+ final VideoCall videoCall = call == null ? null : call.getVideoCall();
+ LogUtil.i(
+ "VideoCallPresenter.changeVideoCall",
+ "videoCall: %s, mVideoCall: %s",
+ videoCall,
+ mVideoCall);
+ final boolean hasChanged = mVideoCall == null && videoCall != null;
+
+ mVideoCall = videoCall;
+ if (mVideoCall == null) {
+ LogUtil.v("VideoCallPresenter.changeVideoCall", "video call or primary call is null. Return");
+ return;
+ }
+
+ if (shouldShowVideoUiForCall(call) && hasChanged) {
+ adjustVideoMode(call);
+ }
+ }
+
+ private boolean isCameraRequired() {
+ return mPrimaryCall != null
+ && isCameraRequired(
+ mPrimaryCall.getVideoState(), mPrimaryCall.getSessionModificationState());
+ }
+
+ /**
+ * Adjusts the current video mode by setting up the preview and display surfaces as necessary.
+ * Expected to be called whenever the video state associated with a call changes (e.g. a user
+ * turns their camera on or off) to ensure the correct surfaces are shown/hidden. TODO: Need
+ * to adjust size and orientation of preview surface here.
+ */
+ private void adjustVideoMode(DialerCall call) {
+ VideoCall videoCall = call.getVideoCall();
+ int newVideoState = call.getVideoState();
+
+ LogUtil.i(
+ "VideoCallPresenter.adjustVideoMode",
+ "videoCall: %s, videoState: %d",
+ videoCall,
+ newVideoState);
+ if (mVideoCallScreen == null) {
+ LogUtil.e("VideoCallPresenter.adjustVideoMode", "error VideoCallScreen is null so returning");
+ return;
+ }
+
+ showVideoUi(
+ newVideoState, call.getState(), call.getSessionModificationState(), call.isRemotelyHeld());
+
+ // Communicate the current camera to telephony and make a request for the camera
+ // capabilities.
+ if (videoCall != null) {
+ Surface surface = getRemoteVideoSurfaceTexture().getSavedSurface();
+ if (surface != null) {
+ LogUtil.v(
+ "VideoCallPresenter.adjustVideoMode", "calling setDisplaySurface with: " + surface);
+ videoCall.setDisplaySurface(surface);
+ }
+
+ Assert.checkState(
+ mDeviceOrientation != InCallOrientationEventListener.SCREEN_ORIENTATION_UNKNOWN);
+ videoCall.setDeviceOrientation(mDeviceOrientation);
+ enableCamera(videoCall, isCameraRequired(newVideoState, call.getSessionModificationState()));
+ }
+ int previousVideoState = mCurrentVideoState;
+ mCurrentVideoState = newVideoState;
+ mIsVideoMode = true;
+
+ // adjustVideoMode may be called if we are already in a 1-way video state. In this case
+ // we do not want to trigger auto-fullscreen mode.
+ if (!VideoUtils.isVideoCall(previousVideoState) && VideoUtils.isVideoCall(newVideoState)) {
+ maybeAutoEnterFullscreen(call);
+ }
+ }
+
+ private static boolean shouldShowVideoUiForCall(@Nullable DialerCall call) {
+ if (call == null) {
+ return false;
+ }
+
+ if (VideoUtils.isVideoCall(call)) {
+ return true;
+ }
+
+ if (isVideoUpgrade(call)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private void enableCamera(VideoCall videoCall, boolean isCameraRequired) {
+ LogUtil.v(
+ "VideoCallPresenter.enableCamera",
+ "videoCall: %s, enabling: %b",
+ videoCall,
+ isCameraRequired);
+ if (videoCall == null) {
+ LogUtil.i("VideoCallPresenter.enableCamera", "videoCall is null.");
+ return;
+ }
+
+ boolean hasCameraPermission = VideoUtils.hasCameraPermissionAndAllowedByUser(mContext);
+ if (!hasCameraPermission) {
+ videoCall.setCamera(null);
+ mPreviewSurfaceState = PreviewSurfaceState.NONE;
+ // TODO: Inform remote party that the video is off. This is similar to b/30256571.
+ } else if (isCameraRequired) {
+ InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager();
+ videoCall.setCamera(cameraManager.getActiveCameraId());
+ mPreviewSurfaceState = PreviewSurfaceState.CAMERA_SET;
+ videoCall.requestCameraCapabilities();
+ } else {
+ mPreviewSurfaceState = PreviewSurfaceState.NONE;
+ videoCall.setCamera(null);
+ }
+ }
+
+ /** Exits video mode by hiding the video surfaces and making other adjustments (eg. audio). */
+ private void exitVideoMode() {
+ LogUtil.i("VideoCallPresenter.exitVideoMode", "");
+
+ showVideoUi(
+ VideoProfile.STATE_AUDIO_ONLY,
+ DialerCall.State.ACTIVE,
+ DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST,
+ false /* isRemotelyHeld */);
+ enableCamera(mVideoCall, false);
+ InCallPresenter.getInstance().setFullScreen(false);
+
+ mIsVideoMode = false;
+ }
+
+ /**
+ * Based on the current video state and call state, show or hide the incoming and outgoing video
+ * surfaces. The outgoing video surface is shown any time video is transmitting. The incoming
+ * video surface is shown whenever the video is un-paused and active.
+ *
+ * @param videoState The video state.
+ * @param callState The call state.
+ */
+ private void showVideoUi(
+ int videoState,
+ int callState,
+ @SessionModificationState int sessionModificationState,
+ boolean isRemotelyHeld) {
+ if (mVideoCallScreen == null) {
+ LogUtil.e("VideoCallPresenter.showVideoUi", "videoCallScreen is null returning");
+ return;
+ }
+ boolean showIncomingVideo = showIncomingVideo(videoState, callState);
+ boolean showOutgoingVideo = showOutgoingVideo(mContext, videoState, sessionModificationState);
+ LogUtil.i(
+ "VideoCallPresenter.showVideoUi",
+ "showIncoming: %b, showOutgoing: %b, isRemotelyHeld: %b",
+ showIncomingVideo,
+ showOutgoingVideo,
+ isRemotelyHeld);
+ updateRemoteVideoSurfaceDimensions();
+ mVideoCallScreen.showVideoViews(showOutgoingVideo, showIncomingVideo, isRemotelyHeld);
+
+ InCallPresenter.getInstance().enableScreenTimeout(VideoProfile.isAudioOnly(videoState));
+ updateFullscreenAndGreenScreenMode(callState, sessionModificationState);
+ }
+
+ /**
+ * Handles peer video pause state changes.
+ *
+ * @param call The call which paused or un-pausedvideo transmission.
+ * @param paused {@code True} when the video transmission is paused, {@code false} when video
+ * transmission resumes.
+ */
+ @Override
+ public void onPeerPauseStateChanged(DialerCall call, boolean paused) {
+ if (!call.equals(mPrimaryCall)) {
+ return;
+ }
+ }
+
+ /**
+ * Handles peer video dimension changes.
+ *
+ * @param call The call which experienced a peer video dimension change.
+ * @param width The new peer video width .
+ * @param height The new peer video height.
+ */
+ @Override
+ public void onUpdatePeerDimensions(DialerCall call, int width, int height) {
+ LogUtil.i("VideoCallPresenter.onUpdatePeerDimensions", "width: %d, height: %d", width, height);
+ if (mVideoCallScreen == null) {
+ LogUtil.e("VideoCallPresenter.onUpdatePeerDimensions", "videoCallScreen is null");
+ return;
+ }
+ if (!call.equals(mPrimaryCall)) {
+ LogUtil.e(
+ "VideoCallPresenter.onUpdatePeerDimensions", "current call is not equal to primary");
+ return;
+ }
+
+ // Change size of display surface to match the peer aspect ratio
+ if (width > 0 && height > 0 && mVideoCallScreen != null) {
+ getRemoteVideoSurfaceTexture().setSourceVideoDimensions(new Point(width, height));
+ mVideoCallScreen.onRemoteVideoDimensionsChanged();
+ }
+ }
+
+ /**
+ * Handles any video quality changes in the call.
+ *
+ * @param call The call which experienced a video quality change.
+ * @param videoQuality The new video call quality.
+ */
+ @Override
+ public void onVideoQualityChanged(DialerCall call, int videoQuality) {
+ // No-op
+ }
+
+ /**
+ * Handles a change to the dimensions of the local camera. Receiving the camera capabilities
+ * triggers the creation of the video
+ *
+ * @param call The call which experienced the camera dimension change.
+ * @param width The new camera video width.
+ * @param height The new camera video height.
+ */
+ @Override
+ public void onCameraDimensionsChange(DialerCall call, int width, int height) {
+ LogUtil.i(
+ "VideoCallPresenter.onCameraDimensionsChange",
+ "call: %s, width: %d, height: %d",
+ call,
+ width,
+ height);
+ if (mVideoCallScreen == null) {
+ LogUtil.e("VideoCallPresenter.onCameraDimensionsChange", "ui is null");
+ return;
+ }
+
+ if (!call.equals(mPrimaryCall)) {
+ LogUtil.e("VideoCallPresenter.onCameraDimensionsChange", "not the primary call");
+ return;
+ }
+
+ mPreviewSurfaceState = PreviewSurfaceState.CAPABILITIES_RECEIVED;
+ changePreviewDimensions(width, height);
+
+ // Check if the preview surface is ready yet; if it is, set it on the {@code VideoCall}.
+ // If it not yet ready, it will be set when when creation completes.
+ Surface surface = getLocalVideoSurfaceTexture().getSavedSurface();
+ if (surface != null) {
+ mPreviewSurfaceState = PreviewSurfaceState.SURFACE_SET;
+ mVideoCall.setPreviewSurface(surface);
+ }
+ }
+
+ /**
+ * Changes the dimensions of the preview surface.
+ *
+ * @param width The new width.
+ * @param height The new height.
+ */
+ private void changePreviewDimensions(int width, int height) {
+ if (mVideoCallScreen == null) {
+ return;
+ }
+
+ // Resize the surface used to display the preview video
+ getLocalVideoSurfaceTexture().setSurfaceDimensions(new Point(width, height));
+ mVideoCallScreen.onLocalVideoDimensionsChanged();
+ }
+
+ /**
+ * Called when call session event is raised.
+ *
+ * @param event The call session event.
+ */
+ @Override
+ public void onCallSessionEvent(int event) {
+ switch (event) {
+ case Connection.VideoProvider.SESSION_EVENT_RX_PAUSE:
+ LogUtil.v("VideoCallPresenter.onCallSessionEvent", "rx_pause");
+ break;
+ case Connection.VideoProvider.SESSION_EVENT_RX_RESUME:
+ LogUtil.v("VideoCallPresenter.onCallSessionEvent", "rx_resume");
+ break;
+ case Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE:
+ LogUtil.v("VideoCallPresenter.onCallSessionEvent", "camera_failure");
+ break;
+ case Connection.VideoProvider.SESSION_EVENT_CAMERA_READY:
+ LogUtil.v("VideoCallPresenter.onCallSessionEvent", "camera_ready");
+ break;
+ default:
+ LogUtil.v("VideoCallPresenter.onCallSessionEvent", "unknown event = : " + event);
+ break;
+ }
+ }
+
+ /**
+ * Handles a change to the call data usage
+ *
+ * @param dataUsage call data usage value
+ */
+ @Override
+ public void onCallDataUsageChange(long dataUsage) {
+ LogUtil.v("VideoCallPresenter.onCallDataUsageChange", "dataUsage=" + dataUsage);
+ }
+
+ /**
+ * Handles changes to the device orientation.
+ *
+ * @param orientation The screen orientation of the device (one of: {@link
+ * InCallOrientationEventListener#SCREEN_ORIENTATION_0}, {@link
+ * InCallOrientationEventListener#SCREEN_ORIENTATION_90}, {@link
+ * InCallOrientationEventListener#SCREEN_ORIENTATION_180}, {@link
+ * InCallOrientationEventListener#SCREEN_ORIENTATION_270}).
+ */
+ @Override
+ public void onDeviceOrientationChanged(int orientation) {
+ LogUtil.i(
+ "VideoCallPresenter.onDeviceOrientationChanged",
+ "orientation: %d -> %d",
+ mDeviceOrientation,
+ orientation);
+ mDeviceOrientation = orientation;
+
+ if (mVideoCallScreen == null) {
+ LogUtil.e("VideoCallPresenter.onDeviceOrientationChanged", "videoCallScreen is null");
+ return;
+ }
+
+ Point previewDimensions = getLocalVideoSurfaceTexture().getSurfaceDimensions();
+ if (previewDimensions == null) {
+ return;
+ }
+ LogUtil.v(
+ "VideoCallPresenter.onDeviceOrientationChanged",
+ "orientation: %d, size: %s",
+ orientation,
+ previewDimensions);
+ changePreviewDimensions(previewDimensions.x, previewDimensions.y);
+
+ mVideoCallScreen.onLocalVideoOrientationChanged();
+ }
+
+ /**
+ * Exits fullscreen mode if the current call context has changed to a non-video call.
+ *
+ * @param call The call.
+ */
+ protected void maybeExitFullscreen(DialerCall call) {
+ if (call == null) {
+ return;
+ }
+
+ if (!VideoUtils.isVideoCall(call) || call.getState() == DialerCall.State.INCOMING) {
+ LogUtil.i("VideoCallPresenter.maybeExitFullscreen", "exiting fullscreen");
+ InCallPresenter.getInstance().setFullScreen(false);
+ }
+ }
+
+ /**
+ * Schedules auto-entering of fullscreen mode. Will not enter full screen mode if any of the
+ * following conditions are met: 1. No call 2. DialerCall is not active 3. The current video state
+ * is not bi-directional. 4. Already in fullscreen mode 5. In accessibility mode
+ *
+ * @param call The current call.
+ */
+ protected void maybeAutoEnterFullscreen(DialerCall call) {
+ if (!mIsAutoFullscreenEnabled) {
+ return;
+ }
+
+ if (call == null
+ || call.getState() != DialerCall.State.ACTIVE
+ || !VideoUtils.isBidirectionalVideoCall(call)
+ || InCallPresenter.getInstance().isFullscreen()
+ || (mContext != null && AccessibilityUtil.isTouchExplorationEnabled(mContext))) {
+ // Ensure any previously scheduled attempt to enter fullscreen is cancelled.
+ cancelAutoFullScreen();
+ return;
+ }
+
+ if (mAutoFullScreenPending) {
+ LogUtil.v("VideoCallPresenter.maybeAutoEnterFullscreen", "already pending.");
+ return;
+ }
+ LogUtil.v("VideoCallPresenter.maybeAutoEnterFullscreen", "scheduled");
+ mAutoFullScreenPending = true;
+ mHandler.removeCallbacks(mAutoFullscreenRunnable);
+ mHandler.postDelayed(mAutoFullscreenRunnable, mAutoFullscreenTimeoutMillis);
+ }
+
+ /** Cancels pending auto fullscreen mode. */
+ @Override
+ public void cancelAutoFullScreen() {
+ if (!mAutoFullScreenPending) {
+ LogUtil.v("VideoCallPresenter.cancelAutoFullScreen", "none pending.");
+ return;
+ }
+ LogUtil.v("VideoCallPresenter.cancelAutoFullScreen", "cancelling pending");
+ mAutoFullScreenPending = false;
+ mHandler.removeCallbacks(mAutoFullscreenRunnable);
+ }
+
+ private void updateRemoteVideoSurfaceDimensions() {
+ Activity activity = mVideoCallScreen.getVideoCallScreenFragment().getActivity();
+ if (activity != null) {
+ Point screenSize = new Point();
+ activity.getWindowManager().getDefaultDisplay().getSize(screenSize);
+ getRemoteVideoSurfaceTexture().setSurfaceDimensions(screenSize);
+ }
+ }
+
+ private static boolean isVideoUpgrade(DialerCall call) {
+ return VideoUtils.hasSentVideoUpgradeRequest(call)
+ || VideoUtils.hasReceivedVideoUpgradeRequest(call);
+ }
+
+ private static boolean isVideoUpgrade(@SessionModificationState int state) {
+ return VideoUtils.hasSentVideoUpgradeRequest(state)
+ || VideoUtils.hasReceivedVideoUpgradeRequest(state);
+ }
+
+ private class LocalDelegate implements VideoSurfaceDelegate {
+ @Override
+ public void onSurfaceCreated(VideoSurfaceTexture videoCallSurface) {
+ if (mVideoCallScreen == null) {
+ LogUtil.e("VideoCallPresenter.LocalDelegate.onSurfaceCreated", "no UI");
+ return;
+ }
+ if (mVideoCall == null) {
+ LogUtil.e("VideoCallPresenter.LocalDelegate.onSurfaceCreated", "no video call");
+ return;
+ }
+
+ // If the preview surface has just been created and we have already received camera
+ // capabilities, but not yet set the surface, we will set the surface now.
+ if (mPreviewSurfaceState == PreviewSurfaceState.CAPABILITIES_RECEIVED) {
+ mPreviewSurfaceState = PreviewSurfaceState.SURFACE_SET;
+ mVideoCall.setPreviewSurface(videoCallSurface.getSavedSurface());
+ } else if (mPreviewSurfaceState == PreviewSurfaceState.NONE && isCameraRequired()) {
+ enableCamera(mVideoCall, true);
+ }
+ }
+
+ @Override
+ public void onSurfaceReleased(VideoSurfaceTexture videoCallSurface) {
+ if (mVideoCall == null) {
+ LogUtil.e("VideoCallPresenter.LocalDelegate.onSurfaceReleased", "no video call");
+ return;
+ }
+
+ mVideoCall.setPreviewSurface(null);
+ enableCamera(mVideoCall, false);
+ }
+
+ @Override
+ public void onSurfaceDestroyed(VideoSurfaceTexture videoCallSurface) {
+ if (mVideoCall == null) {
+ LogUtil.e("VideoCallPresenter.LocalDelegate.onSurfaceDestroyed", "no video call");
+ return;
+ }
+
+ boolean isChangingConfigurations = InCallPresenter.getInstance().isChangingConfigurations();
+ if (!isChangingConfigurations) {
+ enableCamera(mVideoCall, false);
+ } else {
+ LogUtil.i(
+ "VideoCallPresenter.LocalDelegate.onSurfaceDestroyed",
+ "activity is being destroyed due to configuration changes. Not closing the camera.");
+ }
+ }
+
+ @Override
+ public void onSurfaceClick(VideoSurfaceTexture videoCallSurface) {
+ VideoCallPresenter.this.onSurfaceClick();
+ }
+ }
+
+ private class RemoteDelegate implements VideoSurfaceDelegate {
+ @Override
+ public void onSurfaceCreated(VideoSurfaceTexture videoCallSurface) {
+ if (mVideoCallScreen == null) {
+ LogUtil.e("VideoCallPresenter.RemoteDelegate.onSurfaceCreated", "no UI");
+ return;
+ }
+ if (mVideoCall == null) {
+ LogUtil.e("VideoCallPresenter.RemoteDelegate.onSurfaceCreated", "no video call");
+ return;
+ }
+ mVideoCall.setDisplaySurface(videoCallSurface.getSavedSurface());
+ }
+
+ @Override
+ public void onSurfaceReleased(VideoSurfaceTexture videoCallSurface) {
+ if (mVideoCall == null) {
+ LogUtil.e("VideoCallPresenter.RemoteDelegate.onSurfaceReleased", "no video call");
+ return;
+ }
+ mVideoCall.setDisplaySurface(null);
+ }
+
+ @Override
+ public void onSurfaceDestroyed(VideoSurfaceTexture videoCallSurface) {}
+
+ @Override
+ public void onSurfaceClick(VideoSurfaceTexture videoCallSurface) {
+ VideoCallPresenter.this.onSurfaceClick();
+ }
+ }
+
+ /** Defines the state of the preview surface negotiation with the telephony layer. */
+ private static class PreviewSurfaceState {
+
+ /**
+ * The camera has not yet been set on the {@link VideoCall}; negotiation has not yet started.
+ */
+ private static final int NONE = 0;
+
+ /**
+ * The camera has been set on the {@link VideoCall}, but camera capabilities have not yet been
+ * received.
+ */
+ private static final int CAMERA_SET = 1;
+
+ /**
+ * The camera capabilties have been received from telephony, but the surface has not yet been
+ * set on the {@link VideoCall}.
+ */
+ private static final int CAPABILITIES_RECEIVED = 2;
+
+ /** The surface has been set on the {@link VideoCall}. */
+ private static final int SURFACE_SET = 3;
+ }
+}
diff --git a/java/com/android/incallui/VideoPauseController.java b/java/com/android/incallui/VideoPauseController.java
new file mode 100644
index 000000000..2b4357704
--- /dev/null
+++ b/java/com/android/incallui/VideoPauseController.java
@@ -0,0 +1,416 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui;
+
+import android.support.annotation.NonNull;
+import android.telecom.VideoProfile;
+import com.android.incallui.InCallPresenter.InCallState;
+import com.android.incallui.InCallPresenter.InCallStateListener;
+import com.android.incallui.InCallPresenter.IncomingCallListener;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.State;
+import com.android.incallui.call.VideoUtils;
+import java.util.Objects;
+
+/**
+ * This class is responsible for generating video pause/resume requests when the InCall UI is sent
+ * to the background and subsequently brought back to the foreground.
+ */
+class VideoPauseController implements InCallStateListener, IncomingCallListener {
+
+ private static final String TAG = "VideoPauseController";
+ private static VideoPauseController sVideoPauseController;
+ private InCallPresenter mInCallPresenter;
+ /** The current call context, if applicable. */
+ private CallContext mPrimaryCallContext = null;
+ /**
+ * Tracks whether the application is in the background. {@code True} if the application is in the
+ * background, {@code false} otherwise.
+ */
+ private boolean mIsInBackground = false;
+
+ /**
+ * Singleton accessor for the {@link VideoPauseController}.
+ *
+ * @return Singleton instance of the {@link VideoPauseController}.
+ */
+ /*package*/
+ static synchronized VideoPauseController getInstance() {
+ if (sVideoPauseController == null) {
+ sVideoPauseController = new VideoPauseController();
+ }
+ return sVideoPauseController;
+ }
+
+ /**
+ * Determines if a given call is the same one stored in a {@link CallContext}.
+ *
+ * @param call The call.
+ * @param callContext The call context.
+ * @return {@code true} if the {@link DialerCall} is the same as the one referenced in the {@link
+ * CallContext}.
+ */
+ private static boolean areSame(DialerCall call, CallContext callContext) {
+ if (call == null && callContext == null) {
+ return true;
+ } else if (call == null || callContext == null) {
+ return false;
+ }
+ return call.equals(callContext.getCall());
+ }
+
+ /**
+ * Determines if a video call can be paused. Only a video call which is active can be paused.
+ *
+ * @param callContext The call context to check.
+ * @return {@code true} if the call is an active video call.
+ */
+ private static boolean canVideoPause(CallContext callContext) {
+ return isVideoCall(callContext) && callContext.getState() == DialerCall.State.ACTIVE;
+ }
+
+ /**
+ * Determines if a call referenced by a {@link CallContext} is a video call.
+ *
+ * @param callContext The call context.
+ * @return {@code true} if the call is a video call, {@code false} otherwise.
+ */
+ private static boolean isVideoCall(CallContext callContext) {
+ return callContext != null && VideoUtils.isVideoCall(callContext.getVideoState());
+ }
+
+ /**
+ * Determines if call is in incoming/waiting state.
+ *
+ * @param call The call context.
+ * @return {@code true} if the call is in incoming or waiting state, {@code false} otherwise.
+ */
+ private static boolean isIncomingCall(CallContext call) {
+ return call != null && isIncomingCall(call.getCall());
+ }
+
+ /**
+ * Determines if a call is in incoming/waiting state.
+ *
+ * @param call The call.
+ * @return {@code true} if the call is in incoming or waiting state, {@code false} otherwise.
+ */
+ private static boolean isIncomingCall(DialerCall call) {
+ return call != null
+ && (call.getState() == DialerCall.State.CALL_WAITING
+ || call.getState() == DialerCall.State.INCOMING);
+ }
+
+ /**
+ * Determines if a call is dialing.
+ *
+ * @param call The call context.
+ * @return {@code true} if the call is dialing, {@code false} otherwise.
+ */
+ private static boolean isDialing(CallContext call) {
+ return call != null && DialerCall.State.isDialing(call.getState());
+ }
+
+ /**
+ * Configures the {@link VideoPauseController} to listen to call events. Configured via the {@link
+ * com.android.incallui.InCallPresenter}.
+ *
+ * @param inCallPresenter The {@link com.android.incallui.InCallPresenter}.
+ */
+ public void setUp(@NonNull InCallPresenter inCallPresenter) {
+ log("setUp");
+ mInCallPresenter = Objects.requireNonNull(inCallPresenter);
+ mInCallPresenter.addListener(this);
+ mInCallPresenter.addIncomingCallListener(this);
+ }
+
+ /**
+ * Cleans up the {@link VideoPauseController} by removing all listeners and clearing its internal
+ * state. Called from {@link com.android.incallui.InCallPresenter}.
+ */
+ public void tearDown() {
+ log("tearDown...");
+ mInCallPresenter.removeListener(this);
+ mInCallPresenter.removeIncomingCallListener(this);
+ clear();
+ }
+
+ /** Clears the internal state for the {@link VideoPauseController}. */
+ private void clear() {
+ mInCallPresenter = null;
+ mPrimaryCallContext = null;
+ mIsInBackground = false;
+ }
+
+ /**
+ * Handles changes in the {@link InCallState}. Triggers pause and resumption of video for the
+ * current foreground call.
+ *
+ * @param oldState The previous {@link InCallState}.
+ * @param newState The current {@link InCallState}.
+ * @param callList List of current call.
+ */
+ @Override
+ public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
+ log("onStateChange, OldState=" + oldState + " NewState=" + newState);
+
+ DialerCall call;
+ if (newState == InCallState.INCOMING) {
+ call = callList.getIncomingCall();
+ } else if (newState == InCallState.WAITING_FOR_ACCOUNT) {
+ call = callList.getWaitingForAccountCall();
+ } else if (newState == InCallState.PENDING_OUTGOING) {
+ call = callList.getPendingOutgoingCall();
+ } else if (newState == InCallState.OUTGOING) {
+ call = callList.getOutgoingCall();
+ } else {
+ call = callList.getActiveCall();
+ }
+
+ boolean hasPrimaryCallChanged = !areSame(call, mPrimaryCallContext);
+ boolean canVideoPause = VideoUtils.canVideoPause(call);
+ log("onStateChange, hasPrimaryCallChanged=" + hasPrimaryCallChanged);
+ log("onStateChange, canVideoPause=" + canVideoPause);
+ log("onStateChange, IsInBackground=" + mIsInBackground);
+
+ if (hasPrimaryCallChanged) {
+ onPrimaryCallChanged(call);
+ return;
+ }
+
+ if (isDialing(mPrimaryCallContext) && canVideoPause && mIsInBackground) {
+ // Bring UI to foreground if outgoing request becomes active while UI is in
+ // background.
+ bringToForeground();
+ } else if (!isVideoCall(mPrimaryCallContext) && canVideoPause && mIsInBackground) {
+ // Bring UI to foreground if VoLTE call becomes active while UI is in
+ // background.
+ bringToForeground();
+ }
+
+ updatePrimaryCallContext(call);
+ }
+
+ /**
+ * Handles a change to the primary call.
+ *
+ * <p>Reject incoming or hangup dialing call: Where the previous call was an incoming call or a
+ * call in dialing state, resume the new primary call. DialerCall swap: Where the new primary call
+ * is incoming, pause video on the previous primary call.
+ *
+ * @param call The new primary call.
+ */
+ private void onPrimaryCallChanged(DialerCall call) {
+ log("onPrimaryCallChanged: New call = " + call);
+ log("onPrimaryCallChanged: Old call = " + mPrimaryCallContext);
+ log("onPrimaryCallChanged, IsInBackground=" + mIsInBackground);
+
+ if (areSame(call, mPrimaryCallContext)) {
+ throw new IllegalStateException();
+ }
+ final boolean canVideoPause = VideoUtils.canVideoPause(call);
+
+ if ((isIncomingCall(mPrimaryCallContext)
+ || isDialing(mPrimaryCallContext)
+ || (call != null && VideoProfile.isPaused(call.getVideoState())))
+ && canVideoPause
+ && !mIsInBackground) {
+ // Send resume request for the active call, if user rejects incoming call, ends dialing
+ // call, or the call was previously in a paused state and UI is in the foreground.
+ sendRequest(call, true);
+ } else if (isIncomingCall(call) && canVideoPause(mPrimaryCallContext)) {
+ // Send pause request if there is an active video call, and we just received a new
+ // incoming call.
+ sendRequest(mPrimaryCallContext.getCall(), false);
+ }
+
+ updatePrimaryCallContext(call);
+ }
+
+ /**
+ * Handles new incoming calls by triggering a change in the primary call.
+ *
+ * @param oldState the old {@link InCallState}.
+ * @param newState the new {@link InCallState}.
+ * @param call the incoming call.
+ */
+ @Override
+ public void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call) {
+ log("onIncomingCall, OldState=" + oldState + " NewState=" + newState + " DialerCall=" + call);
+
+ if (areSame(call, mPrimaryCallContext)) {
+ return;
+ }
+
+ onPrimaryCallChanged(call);
+ }
+
+ /**
+ * Caches a reference to the primary call and stores its previous state.
+ *
+ * @param call The new primary call.
+ */
+ private void updatePrimaryCallContext(DialerCall call) {
+ if (call == null) {
+ mPrimaryCallContext = null;
+ } else if (mPrimaryCallContext != null) {
+ mPrimaryCallContext.update(call);
+ } else {
+ mPrimaryCallContext = new CallContext(call);
+ }
+ }
+
+ /**
+ * Called when UI goes in/out of the foreground.
+ *
+ * @param showing true if UI is in the foreground, false otherwise.
+ */
+ public void onUiShowing(boolean showing) {
+ if (mInCallPresenter == null) {
+ return;
+ }
+
+ final boolean isInCall = mInCallPresenter.getInCallState() == InCallState.INCALL;
+ if (showing) {
+ onResume(isInCall);
+ } else {
+ onPause(isInCall);
+ }
+ }
+
+ /**
+ * Called when UI is brought to the foreground. Sends a session modification request to resume the
+ * outgoing video.
+ *
+ * @param isInCall {@code true} if we are in an active call. A resume request is only sent to the
+ * video provider if we are in a call.
+ */
+ private void onResume(boolean isInCall) {
+ log("onResume");
+
+ mIsInBackground = false;
+ if (canVideoPause(mPrimaryCallContext) && isInCall) {
+ sendRequest(mPrimaryCallContext.getCall(), true);
+ } else {
+ log("onResume. Ignoring...");
+ }
+ }
+
+ /**
+ * Called when UI is sent to the background. Sends a session modification request to pause the
+ * outgoing video.
+ *
+ * @param isInCall {@code true} if we are in an active call. A pause request is only sent to the
+ * video provider if we are in a call.
+ */
+ private void onPause(boolean isInCall) {
+ log("onPause");
+
+ mIsInBackground = true;
+ if (canVideoPause(mPrimaryCallContext) && isInCall) {
+ sendRequest(mPrimaryCallContext.getCall(), false);
+ } else {
+ log("onPause, Ignoring...");
+ }
+ }
+
+ private void bringToForeground() {
+ if (mInCallPresenter != null) {
+ log("Bringing UI to foreground");
+ mInCallPresenter.bringToForeground(false);
+ } else {
+ loge("InCallPresenter is null. Cannot bring UI to foreground");
+ }
+ }
+
+ /**
+ * Sends Pause/Resume request.
+ *
+ * @param call DialerCall to be paused/resumed.
+ * @param resume If true resume request will be sent, otherwise pause request.
+ */
+ private void sendRequest(DialerCall call, boolean resume) {
+ // Check if this call supports pause/un-pause.
+ if (!call.can(android.telecom.Call.Details.CAPABILITY_CAN_PAUSE_VIDEO)) {
+ return;
+ }
+
+ if (resume) {
+ log("sending resume request, call=" + call);
+ call.getVideoCall().sendSessionModifyRequest(VideoUtils.makeVideoUnPauseProfile(call));
+ } else {
+ log("sending pause request, call=" + call);
+ call.getVideoCall().sendSessionModifyRequest(VideoUtils.makeVideoPauseProfile(call));
+ }
+ }
+
+ /**
+ * Logs a debug message.
+ *
+ * @param msg The message.
+ */
+ private void log(String msg) {
+ Log.d(this, TAG + msg);
+ }
+
+ /**
+ * Logs an error message.
+ *
+ * @param msg The message.
+ */
+ private void loge(String msg) {
+ Log.e(this, TAG + msg);
+ }
+
+ /** Keeps track of the current active/foreground call. */
+ private static class CallContext {
+
+ private int mState = State.INVALID;
+ private int mVideoState;
+ private DialerCall mCall;
+
+ public CallContext(@NonNull DialerCall call) {
+ Objects.requireNonNull(call);
+ update(call);
+ }
+
+ public void update(@NonNull DialerCall call) {
+ mCall = Objects.requireNonNull(call);
+ mState = call.getState();
+ mVideoState = call.getVideoState();
+ }
+
+ public int getState() {
+ return mState;
+ }
+
+ public int getVideoState() {
+ return mVideoState;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "CallContext {CallId=%s, State=%s, VideoState=%d}", mCall.getId(), mState, mVideoState);
+ }
+
+ public DialerCall getCall() {
+ return mCall;
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/bindings/AnswerBindings.java b/java/com/android/incallui/answer/bindings/AnswerBindings.java
new file mode 100644
index 000000000..f7a7a0a95
--- /dev/null
+++ b/java/com/android/incallui/answer/bindings/AnswerBindings.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.bindings;
+
+import com.android.incallui.answer.impl.AnswerFragment;
+import com.android.incallui.answer.protocol.AnswerScreen;
+
+/** Bindings for answer module. */
+public class AnswerBindings {
+
+ public static AnswerScreen createAnswerScreen(
+ String callId, int videoState, boolean isVideoUpgradeRequest) {
+ return AnswerFragment.newInstance(callId, videoState, isVideoUpgradeRequest);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/AffordanceHolderLayout.java b/java/com/android/incallui/answer/impl/AffordanceHolderLayout.java
new file mode 100644
index 000000000..0f93abe68
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/AffordanceHolderLayout.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+import com.android.incallui.answer.impl.affordance.SwipeButtonHelper;
+import com.android.incallui.answer.impl.affordance.SwipeButtonHelper.Callback;
+import com.android.incallui.answer.impl.affordance.SwipeButtonView;
+import com.android.incallui.util.AccessibilityUtil;
+
+/** Layout that delegates touches to its SwipeButtonHelper */
+public class AffordanceHolderLayout extends FrameLayout {
+
+ private SwipeButtonHelper affordanceHelper;
+
+ private Callback affordanceCallback;
+
+ public AffordanceHolderLayout(Context context) {
+ this(context, null);
+ }
+
+ public AffordanceHolderLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AffordanceHolderLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ affordanceHelper =
+ new SwipeButtonHelper(
+ new Callback() {
+ @Override
+ public void onAnimationToSideStarted(
+ boolean rightPage, float translation, float vel) {
+ if (affordanceCallback != null) {
+ affordanceCallback.onAnimationToSideStarted(rightPage, translation, vel);
+ }
+ }
+
+ @Override
+ public void onAnimationToSideEnded() {
+ if (affordanceCallback != null) {
+ affordanceCallback.onAnimationToSideEnded();
+ }
+ }
+
+ @Override
+ public float getMaxTranslationDistance() {
+ if (affordanceCallback != null) {
+ return affordanceCallback.getMaxTranslationDistance();
+ }
+ return 0;
+ }
+
+ @Override
+ public void onSwipingStarted(boolean rightIcon) {
+ if (affordanceCallback != null) {
+ affordanceCallback.onSwipingStarted(rightIcon);
+ }
+ }
+
+ @Override
+ public void onSwipingAborted() {
+ if (affordanceCallback != null) {
+ affordanceCallback.onSwipingAborted();
+ }
+ }
+
+ @Override
+ public void onIconClicked(boolean rightIcon) {
+ if (affordanceCallback != null) {
+ affordanceCallback.onIconClicked(rightIcon);
+ }
+ }
+
+ @Nullable
+ @Override
+ public SwipeButtonView getLeftIcon() {
+ if (affordanceCallback != null) {
+ return affordanceCallback.getLeftIcon();
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public SwipeButtonView getRightIcon() {
+ if (affordanceCallback != null) {
+ return affordanceCallback.getRightIcon();
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public View getLeftPreview() {
+ if (affordanceCallback != null) {
+ return affordanceCallback.getLeftPreview();
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public View getRightPreview() {
+ if (affordanceCallback != null) {
+ affordanceCallback.getRightPreview();
+ }
+ return null;
+ }
+
+ @Override
+ public float getAffordanceFalsingFactor() {
+ if (affordanceCallback != null) {
+ return affordanceCallback.getAffordanceFalsingFactor();
+ }
+ return 1.0f;
+ }
+ },
+ context);
+ }
+
+ public void setAffordanceCallback(@Nullable Callback callback) {
+ affordanceCallback = callback;
+ affordanceHelper.init();
+ }
+
+ public void startHintAnimation(boolean rightIcon, @Nullable Runnable onFinishListener) {
+ affordanceHelper.startHintAnimation(rightIcon, onFinishListener);
+ }
+
+ public void animateHideLeftRightIcon() {
+ affordanceHelper.animateHideLeftRightIcon();
+ }
+
+ public void reset(boolean animate) {
+ affordanceHelper.reset(animate);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) {
+ return false;
+ }
+ return affordanceHelper.onTouchEvent(event) || super.onInterceptTouchEvent(event);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ return affordanceHelper.onTouchEvent(event) || super.onTouchEvent(event);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ affordanceHelper.onConfigurationChanged();
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/AndroidManifest.xml b/java/com/android/incallui/answer/impl/AndroidManifest.xml
new file mode 100644
index 000000000..482c716db
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.incallui.answer.impl">
+</manifest>
diff --git a/java/com/android/incallui/answer/impl/AnswerFragment.java b/java/com/android/incallui/answer/impl/AnswerFragment.java
new file mode 100644
index 000000000..98439ee7f
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/AnswerFragment.java
@@ -0,0 +1,981 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl;
+
+import android.Manifest.permission;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.location.Location;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.FloatRange;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
+import android.support.annotation.VisibleForTesting;
+import android.support.transition.TransitionManager;
+import android.support.v4.app.Fragment;
+import android.telecom.VideoProfile;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.AccessibilityDelegate;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.widget.ImageView;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.FragmentUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.MathUtil;
+import com.android.dialer.compat.ActivityCompat;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.multimedia.MultimediaData;
+import com.android.dialer.util.ViewUtil;
+import com.android.incallui.answer.impl.CreateCustomSmsDialogFragment.CreateCustomSmsHolder;
+import com.android.incallui.answer.impl.SmsBottomSheetFragment.SmsSheetHolder;
+import com.android.incallui.answer.impl.affordance.SwipeButtonHelper.Callback;
+import com.android.incallui.answer.impl.affordance.SwipeButtonView;
+import com.android.incallui.answer.impl.answermethod.AnswerMethod;
+import com.android.incallui.answer.impl.answermethod.AnswerMethodFactory;
+import com.android.incallui.answer.impl.answermethod.AnswerMethodHolder;
+import com.android.incallui.answer.impl.utils.Interpolators;
+import com.android.incallui.answer.protocol.AnswerScreen;
+import com.android.incallui.answer.protocol.AnswerScreenDelegate;
+import com.android.incallui.answer.protocol.AnswerScreenDelegateFactory;
+import com.android.incallui.call.DialerCall.State;
+import com.android.incallui.call.VideoUtils;
+import com.android.incallui.contactgrid.ContactGridManager;
+import com.android.incallui.incall.protocol.ContactPhotoType;
+import com.android.incallui.incall.protocol.InCallScreen;
+import com.android.incallui.incall.protocol.InCallScreenDelegate;
+import com.android.incallui.incall.protocol.InCallScreenDelegateFactory;
+import com.android.incallui.incall.protocol.PrimaryCallState;
+import com.android.incallui.incall.protocol.PrimaryInfo;
+import com.android.incallui.incall.protocol.SecondaryInfo;
+import com.android.incallui.maps.StaticMapBinding;
+import com.android.incallui.sessiondata.AvatarPresenter;
+import com.android.incallui.sessiondata.MultimediaFragment;
+import com.android.incallui.util.AccessibilityUtil;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/** The new version of the incoming call screen. */
+@SuppressLint("ClickableViewAccessibility")
+public class AnswerFragment extends Fragment
+ implements AnswerScreen,
+ InCallScreen,
+ SmsSheetHolder,
+ CreateCustomSmsHolder,
+ AnswerMethodHolder,
+ MultimediaFragment.Holder {
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final String ARG_CALL_ID = "call_id";
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final String ARG_VIDEO_STATE = "video_state";
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final String ARG_IS_VIDEO_UPGRADE_REQUEST = "is_video_upgrade_request";
+
+ private static final String STATE_HAS_ANIMATED_ENTRY = "hasAnimated";
+
+ private static final int HINT_SECONDARY_SHOW_DURATION_MILLIS = 5000;
+ private static final float ANIMATE_LERP_PROGRESS = 0.5f;
+ private static final int STATUS_BAR_DISABLE_RECENT = 0x01000000;
+ private static final int STATUS_BAR_DISABLE_HOME = 0x00200000;
+ private static final int STATUS_BAR_DISABLE_BACK = 0x00400000;
+
+ private static void fadeToward(View view, float newAlpha) {
+ view.setAlpha(MathUtil.lerp(view.getAlpha(), newAlpha, ANIMATE_LERP_PROGRESS));
+ }
+
+ private static void scaleToward(View view, float newScale) {
+ view.setScaleX(MathUtil.lerp(view.getScaleX(), newScale, ANIMATE_LERP_PROGRESS));
+ view.setScaleY(MathUtil.lerp(view.getScaleY(), newScale, ANIMATE_LERP_PROGRESS));
+ }
+
+ private AnswerScreenDelegate answerScreenDelegate;
+ private InCallScreenDelegate inCallScreenDelegate;
+
+ private View importanceBadge;
+ private SwipeButtonView secondaryButton;
+ private AffordanceHolderLayout affordanceHolderLayout;
+ // Use these flags to prevent user from clicking accept/reject buttons multiple times.
+ // We use separate flags because in some rare cases accepting a call may fail to join the room,
+ // and then user is stuck in the incoming call view until it times out. Two flags at least give
+ // the user a chance to get out of the CallActivity.
+ private boolean buttonAcceptClicked;
+ private boolean buttonRejectClicked;
+ private boolean hasAnimatedEntry;
+ private PrimaryInfo primaryInfo = PrimaryInfo.createEmptyPrimaryInfo();
+ private PrimaryCallState primaryCallState;
+ private ArrayList<CharSequence> textResponses;
+ private SmsBottomSheetFragment textResponsesFragment;
+ private CreateCustomSmsDialogFragment createCustomSmsDialogFragment;
+ private SecondaryBehavior secondaryBehavior = SecondaryBehavior.REJECT_WITH_SMS;
+ private ContactGridManager contactGridManager;
+ private AnswerVideoCallScreen answerVideoCallScreen;
+ private Handler handler = new Handler(Looper.getMainLooper());
+
+ private enum SecondaryBehavior {
+ REJECT_WITH_SMS(
+ R.drawable.quantum_ic_message_white_24,
+ R.string.a11y_description_incoming_call_reject_with_sms,
+ R.string.a11y_incoming_call_reject_with_sms,
+ R.string.call_incoming_swipe_to_decline_with_message) {
+ @Override
+ public void performAction(AnswerFragment fragment) {
+ fragment.showMessageMenu();
+ }
+ },
+
+ ANSWER_VIDEO_AS_AUDIO(
+ R.drawable.quantum_ic_videocam_off_white_24,
+ R.string.a11y_description_incoming_call_answer_video_as_audio,
+ R.string.a11y_incoming_call_answer_video_as_audio,
+ R.string.call_incoming_swipe_to_answer_video_as_audio) {
+ @Override
+ public void performAction(AnswerFragment fragment) {
+ fragment.acceptCallByUser(true /* answerVideoAsAudio */);
+ }
+ };
+
+ @DrawableRes public final int icon;
+ @StringRes public final int contentDescription;
+ @StringRes public final int accessibilityLabel;
+ @StringRes public final int hintText;
+
+ SecondaryBehavior(
+ @DrawableRes int icon,
+ @StringRes int contentDescription,
+ @StringRes int accessibilityLabel,
+ @StringRes int hintText) {
+ this.icon = icon;
+ this.contentDescription = contentDescription;
+ this.accessibilityLabel = accessibilityLabel;
+ this.hintText = hintText;
+ }
+
+ public abstract void performAction(AnswerFragment fragment);
+
+ public void applyToView(ImageView view) {
+ view.setImageResource(icon);
+ view.setContentDescription(view.getContext().getText(contentDescription));
+ }
+ }
+
+ private AccessibilityDelegate accessibilityDelegate =
+ new AccessibilityDelegate() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ if (host == secondaryButton) {
+ CharSequence label = getText(secondaryBehavior.accessibilityLabel);
+ info.addAction(new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, label));
+ }
+ }
+
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ if (action == AccessibilityNodeInfo.ACTION_CLICK) {
+ if (host == secondaryButton) {
+ performSecondaryButtonAction();
+ return true;
+ }
+ }
+ return super.performAccessibilityAction(host, action, args);
+ }
+ };
+
+ private Callback affordanceCallback =
+ new Callback() {
+ @Override
+ public void onAnimationToSideStarted(boolean rightPage, float translation, float vel) {}
+
+ @Override
+ public void onAnimationToSideEnded() {
+ performSecondaryButtonAction();
+ }
+
+ @Override
+ public float getMaxTranslationDistance() {
+ View view = getView();
+ if (view == null) {
+ return 0;
+ }
+ return (float) Math.hypot(view.getWidth(), view.getHeight());
+ }
+
+ @Override
+ public void onSwipingStarted(boolean rightIcon) {}
+
+ @Override
+ public void onSwipingAborted() {}
+
+ @Override
+ public void onIconClicked(boolean rightIcon) {
+ affordanceHolderLayout.startHintAnimation(rightIcon, null);
+ getAnswerMethod().setHintText(getText(secondaryBehavior.hintText));
+ handler.removeCallbacks(swipeHintRestoreTimer);
+ handler.postDelayed(swipeHintRestoreTimer, HINT_SECONDARY_SHOW_DURATION_MILLIS);
+ }
+
+ @Override
+ public SwipeButtonView getLeftIcon() {
+ return secondaryButton;
+ }
+
+ @Override
+ public SwipeButtonView getRightIcon() {
+ return null;
+ }
+
+ @Override
+ public View getLeftPreview() {
+ return null;
+ }
+
+ @Override
+ public View getRightPreview() {
+ return null;
+ }
+
+ @Override
+ public float getAffordanceFalsingFactor() {
+ return 1.0f;
+ }
+ };
+
+ private Runnable swipeHintRestoreTimer =
+ new Runnable() {
+ @Override
+ public void run() {
+ restoreSwipeHintTexts();
+ }
+ };
+
+ private void performSecondaryButtonAction() {
+ secondaryBehavior.performAction(this);
+ }
+
+ public static AnswerFragment newInstance(
+ String callId, int videoState, boolean isVideoUpgradeRequest) {
+ Bundle bundle = new Bundle();
+ bundle.putString(ARG_CALL_ID, Assert.isNotNull(callId));
+ bundle.putInt(ARG_VIDEO_STATE, videoState);
+ bundle.putBoolean(ARG_IS_VIDEO_UPGRADE_REQUEST, isVideoUpgradeRequest);
+
+ AnswerFragment instance = new AnswerFragment();
+ instance.setArguments(bundle);
+ return instance;
+ }
+
+ @Override
+ @NonNull
+ public String getCallId() {
+ return Assert.isNotNull(getArguments().getString(ARG_CALL_ID));
+ }
+
+ @Override
+ public int getVideoState() {
+ return getArguments().getInt(ARG_VIDEO_STATE);
+ }
+
+ @Override
+ public boolean isVideoUpgradeRequest() {
+ return getArguments().getBoolean(ARG_IS_VIDEO_UPGRADE_REQUEST);
+ }
+
+ @Override
+ public void setTextResponses(List<String> textResponses) {
+ if (isVideoCall()) {
+ LogUtil.i("AnswerFragment.setTextResponses", "no-op for video calls");
+ } else if (textResponses == null) {
+ LogUtil.i("AnswerFragment.setTextResponses", "no text responses, hiding secondary button");
+ this.textResponses = null;
+ secondaryButton.setVisibility(View.INVISIBLE);
+ } else if (ActivityCompat.isInMultiWindowMode(getActivity())) {
+ LogUtil.i("AnswerFragment.setTextResponses", "in multiwindow, hiding secondary button");
+ this.textResponses = null;
+ secondaryButton.setVisibility(View.INVISIBLE);
+ } else {
+ LogUtil.i("AnswerFragment.setTextResponses", "textResponses.size: " + textResponses.size());
+ this.textResponses = new ArrayList<CharSequence>(textResponses);
+ secondaryButton.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void initSecondaryButton() {
+ secondaryBehavior =
+ isVideoCall() ? SecondaryBehavior.ANSWER_VIDEO_AS_AUDIO : SecondaryBehavior.REJECT_WITH_SMS;
+ secondaryBehavior.applyToView(secondaryButton);
+
+ secondaryButton.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ performSecondaryButtonAction();
+ }
+ });
+ secondaryButton.setClickable(AccessibilityUtil.isAccessibilityEnabled(getContext()));
+ secondaryButton.setFocusable(AccessibilityUtil.isAccessibilityEnabled(getContext()));
+ secondaryButton.setAccessibilityDelegate(accessibilityDelegate);
+
+ if (isVideoCall()) {
+ //noinspection WrongConstant
+ if (!isVideoUpgradeRequest() && VideoProfile.isTransmissionEnabled(getVideoState())) {
+ secondaryButton.setVisibility(View.VISIBLE);
+ } else {
+ secondaryButton.setVisibility(View.INVISIBLE);
+ }
+ }
+ }
+
+ @Override
+ public boolean hasPendingDialogs() {
+ boolean hasPendingDialogs =
+ textResponsesFragment != null || createCustomSmsDialogFragment != null;
+ LogUtil.i("AnswerFragment.hasPendingDialogs", "" + hasPendingDialogs);
+ return hasPendingDialogs;
+ }
+
+ @Override
+ public void dismissPendingDialogs() {
+ LogUtil.i("AnswerFragment.dismissPendingDialogs", null);
+ if (textResponsesFragment != null) {
+ textResponsesFragment.dismiss();
+ textResponsesFragment = null;
+ }
+
+ if (createCustomSmsDialogFragment != null) {
+ createCustomSmsDialogFragment.dismiss();
+ createCustomSmsDialogFragment = null;
+ }
+ }
+
+ @Override
+ public boolean isShowingLocationUi() {
+ Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
+ return fragment != null && fragment.isVisible();
+ }
+
+ @Override
+ public void showLocationUi(@Nullable Fragment locationUi) {
+ boolean isShowing = isShowingLocationUi();
+ if (!isShowing && locationUi != null) {
+ // Show the location fragment.
+ getChildFragmentManager()
+ .beginTransaction()
+ .replace(R.id.incall_location_holder, locationUi)
+ .commitAllowingStateLoss();
+ } else if (isShowing && locationUi == null) {
+ // Hide the location fragment
+ Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
+ getChildFragmentManager().beginTransaction().remove(fragment).commitAllowingStateLoss();
+ }
+ }
+
+ @Override
+ public Fragment getAnswerScreenFragment() {
+ return this;
+ }
+
+ private AnswerMethod getAnswerMethod() {
+ return ((AnswerMethod)
+ getChildFragmentManager().findFragmentById(R.id.answer_method_container));
+ }
+
+ @Override
+ public void setPrimary(PrimaryInfo primaryInfo) {
+ LogUtil.i("AnswerFragment.setPrimary", primaryInfo.toString());
+ this.primaryInfo = primaryInfo;
+ updatePrimaryUI();
+ updateImportanceBadgeVisibility();
+ }
+
+ private void updatePrimaryUI() {
+ if (getView() == null) {
+ return;
+ }
+ contactGridManager.setPrimary(primaryInfo);
+ getAnswerMethod().setShowIncomingWillDisconnect(primaryInfo.answeringDisconnectsOngoingCall);
+ getAnswerMethod()
+ .setContactPhoto(
+ primaryInfo.photoType == ContactPhotoType.CONTACT ? primaryInfo.photo : null);
+ updateDataFragment();
+
+ if (primaryInfo.shouldShowLocation) {
+ // Hide the avatar to make room for location
+ contactGridManager.setAvatarHidden(true);
+ }
+ }
+
+ private void updateDataFragment() {
+ if (!isAdded()) {
+ return;
+ }
+ Fragment current = getChildFragmentManager().findFragmentById(R.id.incall_data_container);
+ Fragment newFragment = null;
+
+ MultimediaData multimediaData = getSessionData();
+ if (multimediaData != null
+ && (!TextUtils.isEmpty(multimediaData.getSubject())
+ || (multimediaData.getImageUri() != null)
+ || (multimediaData.getLocation() != null && canShowMap()))) {
+ // Need message fragment
+ String subject = multimediaData.getSubject();
+ Uri imageUri = multimediaData.getImageUri();
+ Location location = multimediaData.getLocation();
+ if (!(current instanceof MultimediaFragment)
+ || !Objects.equals(((MultimediaFragment) current).getSubject(), subject)
+ || !Objects.equals(((MultimediaFragment) current).getImageUri(), imageUri)
+ || !Objects.equals(((MultimediaFragment) current).getLocation(), location)) {
+ // Needs replacement
+ newFragment =
+ MultimediaFragment.newInstance(
+ multimediaData, false /* isInteractive */, true /* showAvatar */);
+ }
+ } else if (shouldShowAvatar()) {
+ // Needs Avatar
+ if (!(current instanceof AvatarFragment)) {
+ // Needs replacement
+ newFragment = new AvatarFragment();
+ }
+ } else {
+ // Needs empty
+ if (current != null) {
+ getChildFragmentManager().beginTransaction().remove(current).commitNow();
+ }
+ contactGridManager.setAvatarImageView(null, 0, false);
+ }
+
+ if (newFragment != null) {
+ getChildFragmentManager()
+ .beginTransaction()
+ .replace(R.id.incall_data_container, newFragment)
+ .commitNow();
+ }
+ }
+
+ private boolean shouldShowAvatar() {
+ return !isVideoCall();
+ }
+
+ private boolean canShowMap() {
+ return StaticMapBinding.get(getActivity().getApplication()) != null;
+ }
+
+ @Override
+ public void updateAvatar(AvatarPresenter avatarContainer) {
+ contactGridManager.setAvatarImageView(
+ avatarContainer.getAvatarImageView(),
+ avatarContainer.getAvatarSize(),
+ avatarContainer.shouldShowAnonymousAvatar());
+ }
+
+ @Override
+ public void setSecondary(@NonNull SecondaryInfo secondaryInfo) {}
+
+ @Override
+ public void setCallState(@NonNull PrimaryCallState primaryCallState) {
+ LogUtil.i("AnswerFragment.setCallState", primaryCallState.toString());
+ this.primaryCallState = primaryCallState;
+ contactGridManager.setCallState(primaryCallState);
+ }
+
+ @Override
+ public void setEndCallButtonEnabled(boolean enabled, boolean animate) {}
+
+ @Override
+ public void showManageConferenceCallButton(boolean visible) {}
+
+ @Override
+ public boolean isManageConferenceVisible() {
+ return false;
+ }
+
+ @Override
+ public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ contactGridManager.dispatchPopulateAccessibilityEvent(event);
+ // Add prompt of how to accept/decline call with swipe gesture.
+ if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) {
+ event
+ .getText()
+ .add(getResources().getString(R.string.a11y_incoming_call_swipe_gesture_prompt));
+ }
+ }
+
+ @Override
+ public void showNoteSentToast() {}
+
+ @Override
+ public void updateInCallScreenColors() {}
+
+ @Override
+ public void onInCallScreenDialpadVisibilityChange(boolean isShowing) {}
+
+ @Override
+ public int getAnswerAndDialpadContainerResourceId() {
+ Assert.fail();
+ return 0;
+ }
+
+ @Override
+ public Fragment getInCallScreenFragment() {
+ return this;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ Bundle arguments = getArguments();
+ Assert.checkState(arguments.containsKey(ARG_CALL_ID));
+ Assert.checkState(arguments.containsKey(ARG_VIDEO_STATE));
+ Assert.checkState(arguments.containsKey(ARG_IS_VIDEO_UPGRADE_REQUEST));
+
+ buttonAcceptClicked = false;
+ buttonRejectClicked = false;
+
+ View view = inflater.inflate(R.layout.fragment_incoming_call, container, false);
+ secondaryButton = (SwipeButtonView) view.findViewById(R.id.incoming_secondary_button);
+
+ affordanceHolderLayout = (AffordanceHolderLayout) view.findViewById(R.id.incoming_container);
+ affordanceHolderLayout.setAffordanceCallback(affordanceCallback);
+
+ importanceBadge = view.findViewById(R.id.incall_important_call_badge);
+ PillDrawable importanceBackground = new PillDrawable();
+ importanceBackground.setColor(getContext().getColor(android.R.color.white));
+ importanceBadge.setBackground(importanceBackground);
+ importanceBadge
+ .getViewTreeObserver()
+ .addOnGlobalLayoutListener(
+ new OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ int leftRightPadding = importanceBadge.getHeight() / 2;
+ importanceBadge.setPadding(
+ leftRightPadding,
+ importanceBadge.getPaddingTop(),
+ leftRightPadding,
+ importanceBadge.getPaddingBottom());
+ }
+ });
+ updateImportanceBadgeVisibility();
+
+ boolean isVideoCall = isVideoCall();
+ contactGridManager = new ContactGridManager(view, null, 0, false /* showAnonymousAvatar */);
+
+ Fragment answerMethod =
+ getChildFragmentManager().findFragmentById(R.id.answer_method_container);
+ if (AnswerMethodFactory.needsReplacement(answerMethod)) {
+ getChildFragmentManager()
+ .beginTransaction()
+ .replace(
+ R.id.answer_method_container, AnswerMethodFactory.createAnswerMethod(getActivity()))
+ .commitNow();
+ }
+
+ answerScreenDelegate =
+ FragmentUtils.getParentUnsafe(this, AnswerScreenDelegateFactory.class)
+ .newAnswerScreenDelegate(this);
+
+ initSecondaryButton();
+
+ int flags = View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
+ if (!ActivityCompat.isInMultiWindowMode(getActivity())
+ && (getActivity().checkSelfPermission(permission.STATUS_BAR)
+ == PackageManager.PERMISSION_GRANTED)) {
+ LogUtil.i("AnswerFragment.onCreateView", "STATUS_BAR permission granted, disabling nav bar");
+ // These flags will suppress the alert that the activity is in full view mode
+ // during an incoming call on a fresh system/factory reset of the app
+ flags |= STATUS_BAR_DISABLE_BACK | STATUS_BAR_DISABLE_HOME | STATUS_BAR_DISABLE_RECENT;
+ }
+ view.setSystemUiVisibility(flags);
+ if (isVideoCall) {
+ if (VideoUtils.hasCameraPermissionAndAllowedByUser(getContext())) {
+ answerVideoCallScreen = new AnswerVideoCallScreen(this, view);
+ } else {
+ view.findViewById(R.id.videocall_video_off).setVisibility(View.VISIBLE);
+ }
+ }
+
+ return view;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ FragmentUtils.checkParent(this, InCallScreenDelegateFactory.class);
+ }
+
+ @Override
+ public void onViewCreated(final View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ createInCallScreenDelegate();
+ updateUI();
+
+ if (savedInstanceState == null || !savedInstanceState.getBoolean(STATE_HAS_ANIMATED_ENTRY)) {
+ ViewUtil.doOnPreDraw(view, false, this::animateEntry);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ LogUtil.i("AnswerFragment.onResume", null);
+ inCallScreenDelegate.onInCallScreenResumed();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ LogUtil.i("AnswerFragment.onStart", null);
+
+ updateUI();
+ if (answerVideoCallScreen != null) {
+ answerVideoCallScreen.onStart();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ LogUtil.i("AnswerFragment.onStop", null);
+
+ handler.removeCallbacks(swipeHintRestoreTimer);
+ if (answerVideoCallScreen != null) {
+ answerVideoCallScreen.onStop();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ LogUtil.i("AnswerFragment.onPause", null);
+ }
+
+ @Override
+ public void onDestroyView() {
+ LogUtil.i("AnswerFragment.onDestroyView", null);
+ if (answerVideoCallScreen != null) {
+ answerVideoCallScreen = null;
+ }
+ super.onDestroyView();
+ inCallScreenDelegate.onInCallScreenUnready();
+ answerScreenDelegate.onAnswerScreenUnready();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle bundle) {
+ super.onSaveInstanceState(bundle);
+ bundle.putBoolean(STATE_HAS_ANIMATED_ENTRY, hasAnimatedEntry);
+ }
+
+ private void updateUI() {
+ if (getView() == null) {
+ return;
+ }
+
+ if (primaryInfo != null) {
+ updatePrimaryUI();
+ }
+ if (primaryCallState != null) {
+ contactGridManager.setCallState(primaryCallState);
+ }
+
+ restoreBackgroundMaskColor();
+ }
+
+ @Override
+ public boolean isVideoCall() {
+ return VideoUtils.isVideoCall(getVideoState());
+ }
+
+ @Override
+ public void onAnswerProgressUpdate(@FloatRange(from = -1f, to = 1f) float answerProgress) {
+ // Don't fade the window background for call waiting or video upgrades. Fading the background
+ // shows the system wallpaper which looks bad because on reject we switch to another call.
+ if (primaryCallState.state == State.INCOMING && !isVideoCall()) {
+ answerScreenDelegate.updateWindowBackgroundColor(answerProgress);
+ }
+
+ // Fade and scale contact name and video call text
+ float startDelay = .25f;
+ // Header progress is zero over positiveAdjustedProgress = [0, startDelay],
+ // linearly increases over (startDelay, 1] until reaching 1 when positiveAdjustedProgress = 1
+ float headerProgress = Math.max(0, (Math.abs(answerProgress) - 1) / (1 - startDelay) + 1);
+ fadeToward(contactGridManager.getContainerView(), 1 - headerProgress);
+ scaleToward(contactGridManager.getContainerView(), MathUtil.lerp(1f, .75f, headerProgress));
+
+ if (Math.abs(answerProgress) >= .0001) {
+ affordanceHolderLayout.animateHideLeftRightIcon();
+ handler.removeCallbacks(swipeHintRestoreTimer);
+ restoreSwipeHintTexts();
+ }
+ }
+
+ @Override
+ public void answerFromMethod() {
+ acceptCallByUser(false /* answerVideoAsAudio */);
+ }
+
+ @Override
+ public void rejectFromMethod() {
+ rejectCall();
+ }
+
+ @Override
+ public void resetAnswerProgress() {
+ affordanceHolderLayout.reset(true);
+ restoreBackgroundMaskColor();
+ }
+
+ private void animateEntry(@NonNull View rootView) {
+ contactGridManager.getContainerView().setAlpha(0f);
+ Animator alpha =
+ ObjectAnimator.ofFloat(contactGridManager.getContainerView(), View.ALPHA, 0, 1);
+ Animator topRow = createTranslation(rootView.findViewById(R.id.contactgrid_top_row));
+ Animator contactName = createTranslation(rootView.findViewById(R.id.contactgrid_contact_name));
+ Animator bottomRow = createTranslation(rootView.findViewById(R.id.contactgrid_bottom_row));
+ Animator important = createTranslation(importanceBadge);
+ Animator dataContainer = createTranslation(rootView.findViewById(R.id.incall_data_container));
+
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet
+ .play(alpha)
+ .with(topRow)
+ .with(contactName)
+ .with(bottomRow)
+ .with(important)
+ .with(dataContainer);
+ animatorSet.setDuration(getResources().getInteger(android.R.integer.config_longAnimTime));
+ animatorSet.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ hasAnimatedEntry = true;
+ }
+ });
+ animatorSet.start();
+ }
+
+ private ObjectAnimator createTranslation(View view) {
+ float translationY = view.getTop() * 0.5f;
+ ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, translationY, 0);
+ animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+ return animator;
+ }
+
+ private void acceptCallByUser(boolean answerVideoAsAudio) {
+ LogUtil.i("AnswerFragment.acceptCallByUser", answerVideoAsAudio ? " answerVideoAsAudio" : "");
+ if (!buttonAcceptClicked) {
+ int desiredVideoState = getVideoState();
+ if (answerVideoAsAudio) {
+ desiredVideoState = VideoProfile.STATE_AUDIO_ONLY;
+ }
+
+ // Notify the lower layer first to start signaling ASAP.
+ answerScreenDelegate.onAnswer(desiredVideoState);
+
+ buttonAcceptClicked = true;
+ }
+ }
+
+ private void rejectCall() {
+ LogUtil.i("AnswerFragment.rejectCall", null);
+ if (!buttonRejectClicked) {
+ Context context = getContext();
+ if (context == null) {
+ LogUtil.w(
+ "AnswerFragment.rejectCall",
+ "Null context when rejecting call. Logger call was skipped");
+ } else {
+ Logger.get(context)
+ .logImpression(DialerImpression.Type.REJECT_INCOMING_CALL_FROM_ANSWER_SCREEN);
+ }
+ buttonRejectClicked = true;
+ answerScreenDelegate.onReject();
+ }
+ }
+
+ private void restoreBackgroundMaskColor() {
+ answerScreenDelegate.updateWindowBackgroundColor(0);
+ }
+
+ private void restoreSwipeHintTexts() {
+ if (getAnswerMethod() != null) {
+ getAnswerMethod().setHintText(null);
+ }
+ }
+
+ private void showMessageMenu() {
+ LogUtil.i("AnswerFragment.showMessageMenu", "Show sms menu.");
+
+ textResponsesFragment = SmsBottomSheetFragment.newInstance(textResponses);
+ textResponsesFragment.show(getChildFragmentManager(), null);
+ secondaryButton
+ .animate()
+ .alpha(0)
+ .withEndAction(
+ new Runnable() {
+ @Override
+ public void run() {
+ affordanceHolderLayout.reset(false);
+ secondaryButton.animate().alpha(1);
+ }
+ });
+ }
+
+ @Override
+ public void smsSelected(@Nullable CharSequence text) {
+ LogUtil.i("AnswerFragment.smsSelected", null);
+ textResponsesFragment = null;
+
+ if (text == null) {
+ createCustomSmsDialogFragment = CreateCustomSmsDialogFragment.newInstance();
+ createCustomSmsDialogFragment.show(getChildFragmentManager(), null);
+ return;
+ }
+
+ if (primaryCallState != null && canRejectCallWithSms()) {
+ rejectCall();
+ answerScreenDelegate.onRejectCallWithMessage(text.toString());
+ }
+ }
+
+ @Override
+ public void smsDismissed() {
+ LogUtil.i("AnswerFragment.smsDismissed", null);
+ textResponsesFragment = null;
+ answerScreenDelegate.onDismissDialog();
+ }
+
+ @Override
+ public void customSmsCreated(@NonNull CharSequence text) {
+ LogUtil.i("AnswerFragment.customSmsCreated", null);
+ createCustomSmsDialogFragment = null;
+ if (primaryCallState != null && canRejectCallWithSms()) {
+ rejectCall();
+ answerScreenDelegate.onRejectCallWithMessage(text.toString());
+ }
+ }
+
+ @Override
+ public void customSmsDismissed() {
+ LogUtil.i("AnswerFragment.customSmsDismissed", null);
+ createCustomSmsDialogFragment = null;
+ answerScreenDelegate.onDismissDialog();
+ }
+
+ private boolean canRejectCallWithSms() {
+ return primaryCallState != null
+ && !(primaryCallState.state == State.DISCONNECTED
+ || primaryCallState.state == State.DISCONNECTING
+ || primaryCallState.state == State.IDLE);
+ }
+
+ private void createInCallScreenDelegate() {
+ inCallScreenDelegate =
+ FragmentUtils.getParentUnsafe(this, InCallScreenDelegateFactory.class)
+ .newInCallScreenDelegate();
+ Assert.isNotNull(inCallScreenDelegate);
+ inCallScreenDelegate.onInCallScreenDelegateInit(this);
+ inCallScreenDelegate.onInCallScreenReady();
+ }
+
+ private void updateImportanceBadgeVisibility() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (!getResources().getBoolean(R.bool.answer_important_call_allowed)) {
+ importanceBadge.setVisibility(View.GONE);
+ return;
+ }
+
+ MultimediaData multimediaData = getSessionData();
+ boolean showImportant = multimediaData != null && multimediaData.isImportant();
+ TransitionManager.beginDelayedTransition((ViewGroup) importanceBadge.getParent());
+ // TODO (keyboardr): Change this back to being View.INVISIBLE once mocks are available to
+ // properly handle smaller screens
+ importanceBadge.setVisibility(showImportant ? View.VISIBLE : View.GONE);
+ }
+
+ @Nullable
+ private MultimediaData getSessionData() {
+ if (primaryInfo == null) {
+ return null;
+ }
+ return primaryInfo.multimediaData;
+ }
+
+ /** Shows the Avatar image if available. */
+ public static class AvatarFragment extends Fragment implements AvatarPresenter {
+
+ private ImageView avatarImageView;
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+ return layoutInflater.inflate(R.layout.fragment_avatar, viewGroup, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle bundle) {
+ super.onViewCreated(view, bundle);
+ avatarImageView = ((ImageView) view.findViewById(R.id.contactgrid_avatar));
+ FragmentUtils.getParentUnsafe(this, MultimediaFragment.Holder.class).updateAvatar(this);
+ }
+
+ @NonNull
+ @Override
+ public ImageView getAvatarImageView() {
+ return avatarImageView;
+ }
+
+ @Override
+ public int getAvatarSize() {
+ return getResources().getDimensionPixelSize(R.dimen.answer_avatar_size);
+ }
+
+ @Override
+ public boolean shouldShowAnonymousAvatar() {
+ return false;
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java b/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java
new file mode 100644
index 000000000..0316a5fab
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl;
+
+import android.content.res.Configuration;
+import android.graphics.Point;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import android.view.TextureView;
+import android.view.View;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.FragmentUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.video.protocol.VideoCallScreen;
+import com.android.incallui.video.protocol.VideoCallScreenDelegate;
+import com.android.incallui.video.protocol.VideoCallScreenDelegateFactory;
+import com.android.incallui.videosurface.bindings.VideoSurfaceBindings;
+
+/** Shows a video preview for an incoming call. */
+public class AnswerVideoCallScreen implements VideoCallScreen {
+ @NonNull private final Fragment fragment;
+ @NonNull private final TextureView textureView;
+ @NonNull private final VideoCallScreenDelegate delegate;
+
+ public AnswerVideoCallScreen(@NonNull Fragment fragment, @NonNull View view) {
+ this.fragment = fragment;
+
+ textureView =
+ Assert.isNotNull((TextureView) view.findViewById(R.id.incoming_preview_texture_view));
+ View overlayView =
+ Assert.isNotNull(view.findViewById(R.id.incoming_preview_texture_view_overlay));
+ view.setBackgroundColor(0xff000000);
+ delegate =
+ FragmentUtils.getParentUnsafe(fragment, VideoCallScreenDelegateFactory.class)
+ .newVideoCallScreenDelegate();
+ delegate.initVideoCallScreenDelegate(fragment.getContext(), this);
+
+ textureView.setVisibility(View.VISIBLE);
+ overlayView.setVisibility(View.VISIBLE);
+ }
+
+ public void onStart() {
+ LogUtil.i("AnswerVideoCallScreen.onStart", null);
+ delegate.onVideoCallScreenUiReady();
+ delegate.getLocalVideoSurfaceTexture().attachToTextureView(textureView);
+ }
+
+ public void onStop() {
+ LogUtil.i("AnswerVideoCallScreen.onStop", null);
+ delegate.onVideoCallScreenUiUnready();
+ }
+
+ @Override
+ public void showVideoViews(
+ boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld) {
+ LogUtil.i(
+ "AnswerVideoCallScreen.showVideoViews",
+ "showPreview: %b, shouldShowRemote: %b",
+ shouldShowPreview,
+ shouldShowRemote);
+ }
+
+ @Override
+ public void onLocalVideoDimensionsChanged() {
+ LogUtil.i("AnswerVideoCallScreen.onLocalVideoDimensionsChanged", null);
+ updatePreviewVideoScaling();
+ }
+
+ @Override
+ public void onRemoteVideoDimensionsChanged() {}
+
+ @Override
+ public void onLocalVideoOrientationChanged() {
+ LogUtil.i("AnswerVideoCallScreen.onLocalVideoOrientationChanged", null);
+ updatePreviewVideoScaling();
+ }
+
+ @Override
+ public void updateFullscreenAndGreenScreenMode(
+ boolean shouldShowFullscreen, boolean shouldShowGreenScreen) {}
+
+ @Override
+ public Fragment getVideoCallScreenFragment() {
+ return fragment;
+ }
+
+ private void updatePreviewVideoScaling() {
+ if (textureView.getWidth() == 0 || textureView.getHeight() == 0) {
+ LogUtil.i(
+ "AnswerVideoCallScreen.updatePreviewVideoScaling", "view layout hasn't finished yet");
+ return;
+ }
+ Point cameraDimensions = delegate.getLocalVideoSurfaceTexture().getSurfaceDimensions();
+ if (cameraDimensions == null) {
+ LogUtil.i("AnswerVideoCallScreen.updatePreviewVideoScaling", "camera dimensions not set");
+ return;
+ }
+ if (isLandscape()) {
+ VideoSurfaceBindings.scaleVideoAndFillView(
+ textureView, cameraDimensions.x, cameraDimensions.y, delegate.getDeviceOrientation());
+ } else {
+ // Landscape, so dimensions are swapped
+ //noinspection SuspiciousNameCombination
+ VideoSurfaceBindings.scaleVideoAndFillView(
+ textureView, cameraDimensions.y, cameraDimensions.x, delegate.getDeviceOrientation());
+ }
+ }
+
+ private boolean isLandscape() {
+ return fragment.getResources().getConfiguration().orientation
+ == Configuration.ORIENTATION_LANDSCAPE;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/CreateCustomSmsDialogFragment.java b/java/com/android/incallui/answer/impl/CreateCustomSmsDialogFragment.java
new file mode 100644
index 000000000..b49409258
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/CreateCustomSmsDialogFragment.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.answer.impl;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnShowListener;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AppCompatDialogFragment;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.View;
+import android.view.WindowManager.LayoutParams;
+import android.widget.Button;
+import android.widget.EditText;
+import com.android.dialer.common.FragmentUtils;
+
+/**
+ * Shows the dialog for users to enter a custom message when rejecting a call with an SMS message.
+ */
+public class CreateCustomSmsDialogFragment extends AppCompatDialogFragment {
+
+ private static final String ARG_ENTERED_TEXT = "enteredText";
+
+ private EditText editText;
+
+ public static CreateCustomSmsDialogFragment newInstance() {
+ return new CreateCustomSmsDialogFragment();
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ super.onCreateDialog(savedInstanceState);
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ View view = View.inflate(builder.getContext(), R.layout.fragment_custom_sms_dialog, null);
+ editText = (EditText) view.findViewById(R.id.custom_sms_input);
+ if (savedInstanceState != null) {
+ editText.setText(savedInstanceState.getCharSequence(ARG_ENTERED_TEXT));
+ }
+ builder
+ .setCancelable(true)
+ .setView(view)
+ .setPositiveButton(
+ R.string.call_incoming_custom_message_send,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ FragmentUtils.getParentUnsafe(
+ CreateCustomSmsDialogFragment.this, CreateCustomSmsHolder.class)
+ .customSmsCreated(editText.getText().toString().trim());
+ dismiss();
+ }
+ })
+ .setNegativeButton(
+ R.string.call_incoming_custom_message_cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ dismiss();
+ }
+ })
+ .setOnCancelListener(
+ new OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialogInterface) {
+ dismiss();
+ }
+ })
+ .setTitle(R.string.call_incoming_respond_via_sms_custom_message);
+ final AlertDialog customMessagePopup = builder.create();
+ customMessagePopup.setOnShowListener(
+ new OnShowListener() {
+ @Override
+ public void onShow(DialogInterface dialogInterface) {
+ ((AlertDialog) dialogInterface)
+ .getButton(AlertDialog.BUTTON_POSITIVE)
+ .setEnabled(false);
+ }
+ });
+
+ editText.addTextChangedListener(
+ new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ Button sendButton = customMessagePopup.getButton(DialogInterface.BUTTON_POSITIVE);
+ sendButton.setEnabled(editable != null && editable.toString().trim().length() != 0);
+ }
+ });
+ customMessagePopup.getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+ customMessagePopup.getWindow().addFlags(LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+ return customMessagePopup;
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putCharSequence(ARG_ENTERED_TEXT, editText.getText());
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialogInterface) {
+ super.onDismiss(dialogInterface);
+ FragmentUtils.getParentUnsafe(this, CreateCustomSmsHolder.class).customSmsDismissed();
+ }
+
+ /** Call back for {@link CreateCustomSmsDialogFragment} */
+ public interface CreateCustomSmsHolder {
+
+ void customSmsCreated(@NonNull CharSequence text);
+
+ void customSmsDismissed();
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/PillDrawable.java b/java/com/android/incallui/answer/impl/PillDrawable.java
new file mode 100644
index 000000000..57d84c45f
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/PillDrawable.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl;
+
+import android.graphics.Rect;
+import android.graphics.drawable.GradientDrawable;
+
+/** Draws a pill-shaped background */
+public class PillDrawable extends GradientDrawable {
+
+ public PillDrawable() {
+ super();
+ setShape(RECTANGLE);
+ }
+
+ @Override
+ protected void onBoundsChange(Rect r) {
+ super.onBoundsChange(r);
+ setCornerRadius(r.height() / 2);
+ }
+
+ @Override
+ public void setShape(int shape) {
+ if (shape != GradientDrawable.RECTANGLE) {
+ throw new UnsupportedOperationException("PillDrawable must be a rectangle");
+ }
+ super.setShape(shape);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/SmsBottomSheetFragment.java b/java/com/android/incallui/answer/impl/SmsBottomSheetFragment.java
new file mode 100644
index 000000000..085430ea2
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/SmsBottomSheetFragment.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.design.widget.BottomSheetDialogFragment;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import com.android.dialer.common.DpUtil;
+import com.android.dialer.common.FragmentUtils;
+import com.android.dialer.common.LogUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Shows options for rejecting call with SMS */
+public class SmsBottomSheetFragment extends BottomSheetDialogFragment {
+
+ private static final String ARG_OPTIONS = "options";
+
+ public static SmsBottomSheetFragment newInstance(@Nullable ArrayList<CharSequence> options) {
+ SmsBottomSheetFragment fragment = new SmsBottomSheetFragment();
+ Bundle args = new Bundle();
+ args.putCharSequenceArrayList(ARG_OPTIONS, options);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+ LinearLayout layout = new LinearLayout(getContext());
+ layout.setOrientation(LinearLayout.VERTICAL);
+ List<CharSequence> items = getArguments().getCharSequenceArrayList(ARG_OPTIONS);
+ if (items != null) {
+ for (CharSequence item : items) {
+ layout.addView(newTextViewItem(item));
+ }
+ }
+ layout.addView(newTextViewItem(null));
+ layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
+ return layout;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ FragmentUtils.checkParent(this, SmsSheetHolder.class);
+ }
+
+ @Override
+ public Dialog onCreateDialog(final Bundle savedInstanceState) {
+ LogUtil.i("SmsBottomSheetFragment.onCreateDialog", null);
+ Dialog dialog = super.onCreateDialog(savedInstanceState);
+ dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+ return dialog;
+ }
+
+ private TextView newTextViewItem(@Nullable final CharSequence text) {
+ int[] attrs = new int[] {android.R.attr.selectableItemBackground};
+ Context context = new ContextThemeWrapper(getContext(), getTheme());
+ TypedArray typedArray = context.obtainStyledAttributes(attrs);
+ Drawable background = typedArray.getDrawable(0);
+ //noinspection ResourceType
+ typedArray.recycle();
+
+ TextView textView = new TextView(context);
+ textView.setText(text == null ? getString(R.string.call_incoming_message_custom) : text);
+ int padding = (int) DpUtil.dpToPx(context, 16);
+ textView.setPadding(padding, padding, padding, padding);
+ textView.setBackground(background);
+ textView.setTextColor(context.getColor(R.color.blue_grey_100));
+ textView.setTextAppearance(R.style.TextAppearance_AppCompat_Widget_PopupMenu_Large);
+
+ LayoutParams params =
+ new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ textView.setLayoutParams(params);
+
+ textView.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ FragmentUtils.getParentUnsafe(SmsBottomSheetFragment.this, SmsSheetHolder.class)
+ .smsSelected(text);
+ dismiss();
+ }
+ });
+ return textView;
+ }
+
+ @Override
+ public int getTheme() {
+ return R.style.Theme_Design_Light_BottomSheetDialog;
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialogInterface) {
+ super.onDismiss(dialogInterface);
+ FragmentUtils.getParentUnsafe(this, SmsSheetHolder.class).smsDismissed();
+ }
+
+ /** Callback interface for {@link SmsBottomSheetFragment} */
+ public interface SmsSheetHolder {
+
+ void smsSelected(@Nullable CharSequence text);
+
+ void smsDismissed();
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/affordance/AndroidManifest.xml b/java/com/android/incallui/answer/impl/affordance/AndroidManifest.xml
new file mode 100644
index 000000000..960fd71c1
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/affordance/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.incallui.answer.impl.affordance">
+</manifest>
diff --git a/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java b/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java
new file mode 100644
index 000000000..62845b748
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java
@@ -0,0 +1,642 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.affordance;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import com.android.incallui.answer.impl.utils.FlingAnimationUtils;
+import com.android.incallui.answer.impl.utils.Interpolators;
+
+/** A touch handler of the swipe buttons */
+public class SwipeButtonHelper {
+
+ public static final float SWIPE_RESTING_ALPHA_AMOUNT = 0.87f;
+ public static final long HINT_PHASE1_DURATION = 200;
+ private static final long HINT_PHASE2_DURATION = 350;
+ private static final float BACKGROUND_RADIUS_SCALE_FACTOR = 0.25f;
+ private static final int HINT_CIRCLE_OPEN_DURATION = 500;
+
+ private final Context context;
+ private final Callback callback;
+
+ private FlingAnimationUtils flingAnimationUtils;
+ private VelocityTracker velocityTracker;
+ private boolean swipingInProgress;
+ private float initialTouchX;
+ private float initialTouchY;
+ private float translation;
+ private float translationOnDown;
+ private int touchSlop;
+ private int minTranslationAmount;
+ private int minFlingVelocity;
+ private int hintGrowAmount;
+ @Nullable private SwipeButtonView leftIcon;
+ @Nullable private SwipeButtonView rightIcon;
+ private Animator swipeAnimator;
+ private int minBackgroundRadius;
+ private boolean motionCancelled;
+ private int touchTargetSize;
+ private View targetedView;
+ private boolean touchSlopExeeded;
+ private AnimatorListenerAdapter flingEndListener =
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ swipeAnimator = null;
+ swipingInProgress = false;
+ targetedView = null;
+ }
+ };
+ private Runnable animationEndRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ callback.onAnimationToSideEnded();
+ }
+ };
+
+ public SwipeButtonHelper(Callback callback, Context context) {
+ this.context = context;
+ this.callback = callback;
+ init();
+ }
+
+ public void init() {
+ initIcons();
+ updateIcon(
+ leftIcon,
+ 0.0f,
+ leftIcon != null ? leftIcon.getRestingAlpha() : 0,
+ false,
+ false,
+ true,
+ false);
+ updateIcon(
+ rightIcon,
+ 0.0f,
+ rightIcon != null ? rightIcon.getRestingAlpha() : 0,
+ false,
+ false,
+ true,
+ false);
+ initDimens();
+ }
+
+ private void initDimens() {
+ final ViewConfiguration configuration = ViewConfiguration.get(context);
+ touchSlop = configuration.getScaledPagingTouchSlop();
+ minFlingVelocity = configuration.getScaledMinimumFlingVelocity();
+ minTranslationAmount =
+ context.getResources().getDimensionPixelSize(R.dimen.answer_min_swipe_amount);
+ minBackgroundRadius =
+ context
+ .getResources()
+ .getDimensionPixelSize(R.dimen.answer_affordance_min_background_radius);
+ touchTargetSize =
+ context.getResources().getDimensionPixelSize(R.dimen.answer_affordance_touch_target_size);
+ hintGrowAmount =
+ context.getResources().getDimensionPixelSize(R.dimen.hint_grow_amount_sideways);
+ flingAnimationUtils = new FlingAnimationUtils(context, 0.4f);
+ }
+
+ private void initIcons() {
+ leftIcon = callback.getLeftIcon();
+ rightIcon = callback.getRightIcon();
+ updatePreviews();
+ }
+
+ public void updatePreviews() {
+ if (leftIcon != null) {
+ leftIcon.setPreviewView(callback.getLeftPreview());
+ }
+ if (rightIcon != null) {
+ rightIcon.setPreviewView(callback.getRightPreview());
+ }
+ }
+
+ public boolean onTouchEvent(MotionEvent event) {
+ int action = event.getActionMasked();
+ if (motionCancelled && action != MotionEvent.ACTION_DOWN) {
+ return false;
+ }
+ final float y = event.getY();
+ final float x = event.getX();
+
+ boolean isUp = false;
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ View targetView = getIconAtPosition(x, y);
+ if (targetView == null || (targetedView != null && targetedView != targetView)) {
+ motionCancelled = true;
+ return false;
+ }
+ if (targetedView != null) {
+ cancelAnimation();
+ } else {
+ touchSlopExeeded = false;
+ }
+ startSwiping(targetView);
+ initialTouchX = x;
+ initialTouchY = y;
+ translationOnDown = translation;
+ initVelocityTracker();
+ trackMovement(event);
+ motionCancelled = false;
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ motionCancelled = true;
+ endMotion(true /* forceSnapBack */, x, y);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ trackMovement(event);
+ float xDist = x - initialTouchX;
+ float yDist = y - initialTouchY;
+ float distance = (float) Math.hypot(xDist, yDist);
+ if (!touchSlopExeeded && distance > touchSlop) {
+ touchSlopExeeded = true;
+ }
+ if (swipingInProgress) {
+ if (targetedView == rightIcon) {
+ distance = translationOnDown - distance;
+ distance = Math.min(0, distance);
+ } else {
+ distance = translationOnDown + distance;
+ distance = Math.max(0, distance);
+ }
+ setTranslation(distance, false /* isReset */, false /* animateReset */);
+ }
+ break;
+
+ case MotionEvent.ACTION_UP:
+ isUp = true;
+ //fallthrough_intended
+ case MotionEvent.ACTION_CANCEL:
+ boolean hintOnTheRight = targetedView == rightIcon;
+ trackMovement(event);
+ endMotion(!isUp, x, y);
+ if (!touchSlopExeeded && isUp) {
+ callback.onIconClicked(hintOnTheRight);
+ }
+ break;
+ }
+ return true;
+ }
+
+ private void startSwiping(View targetView) {
+ callback.onSwipingStarted(targetView == rightIcon);
+ swipingInProgress = true;
+ targetedView = targetView;
+ }
+
+ private View getIconAtPosition(float x, float y) {
+ if (leftSwipePossible() && isOnIcon(leftIcon, x, y)) {
+ return leftIcon;
+ }
+ if (rightSwipePossible() && isOnIcon(rightIcon, x, y)) {
+ return rightIcon;
+ }
+ return null;
+ }
+
+ public boolean isOnAffordanceIcon(float x, float y) {
+ return isOnIcon(leftIcon, x, y) || isOnIcon(rightIcon, x, y);
+ }
+
+ private boolean isOnIcon(View icon, float x, float y) {
+ float iconX = icon.getX() + icon.getWidth() / 2.0f;
+ float iconY = icon.getY() + icon.getHeight() / 2.0f;
+ double distance = Math.hypot(x - iconX, y - iconY);
+ return distance <= touchTargetSize / 2;
+ }
+
+ private void endMotion(boolean forceSnapBack, float lastX, float lastY) {
+ if (swipingInProgress) {
+ flingWithCurrentVelocity(forceSnapBack, lastX, lastY);
+ } else {
+ targetedView = null;
+ }
+ if (velocityTracker != null) {
+ velocityTracker.recycle();
+ velocityTracker = null;
+ }
+ }
+
+ private boolean rightSwipePossible() {
+ return rightIcon != null && rightIcon.getVisibility() == View.VISIBLE;
+ }
+
+ private boolean leftSwipePossible() {
+ return leftIcon != null && leftIcon.getVisibility() == View.VISIBLE;
+ }
+
+ public void startHintAnimation(boolean right, @Nullable Runnable onFinishedListener) {
+ cancelAnimation();
+ startHintAnimationPhase1(right, onFinishedListener);
+ }
+
+ private void startHintAnimationPhase1(
+ final boolean right, @Nullable final Runnable onFinishedListener) {
+ final SwipeButtonView targetView = right ? rightIcon : leftIcon;
+ ValueAnimator animator = getAnimatorToRadius(right, hintGrowAmount);
+ if (animator == null) {
+ if (onFinishedListener != null) {
+ onFinishedListener.run();
+ }
+ return;
+ }
+ animator.addListener(
+ new AnimatorListenerAdapter() {
+ private boolean mCancelled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mCancelled) {
+ swipeAnimator = null;
+ targetedView = null;
+ if (onFinishedListener != null) {
+ onFinishedListener.run();
+ }
+ } else {
+ startUnlockHintAnimationPhase2(right, onFinishedListener);
+ }
+ }
+ });
+ animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+ animator.setDuration(HINT_PHASE1_DURATION);
+ animator.start();
+ swipeAnimator = animator;
+ targetedView = targetView;
+ }
+
+ /** Phase 2: Move back. */
+ private void startUnlockHintAnimationPhase2(
+ boolean right, @Nullable final Runnable onFinishedListener) {
+ ValueAnimator animator = getAnimatorToRadius(right, 0);
+ if (animator == null) {
+ if (onFinishedListener != null) {
+ onFinishedListener.run();
+ }
+ return;
+ }
+ animator.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ swipeAnimator = null;
+ targetedView = null;
+ if (onFinishedListener != null) {
+ onFinishedListener.run();
+ }
+ }
+ });
+ animator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
+ animator.setDuration(HINT_PHASE2_DURATION);
+ animator.setStartDelay(HINT_CIRCLE_OPEN_DURATION);
+ animator.start();
+ swipeAnimator = animator;
+ }
+
+ private ValueAnimator getAnimatorToRadius(final boolean right, int radius) {
+ final SwipeButtonView targetView = right ? rightIcon : leftIcon;
+ if (targetView == null) {
+ return null;
+ }
+ ValueAnimator animator = ValueAnimator.ofFloat(targetView.getCircleRadius(), radius);
+ animator.addUpdateListener(
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ float newRadius = (float) animation.getAnimatedValue();
+ targetView.setCircleRadiusWithoutAnimation(newRadius);
+ float translation = getTranslationFromRadius(newRadius);
+ SwipeButtonHelper.this.translation = right ? -translation : translation;
+ updateIconsFromTranslation(targetView);
+ }
+ });
+ return animator;
+ }
+
+ private void cancelAnimation() {
+ if (swipeAnimator != null) {
+ swipeAnimator.cancel();
+ }
+ }
+
+ private void flingWithCurrentVelocity(boolean forceSnapBack, float lastX, float lastY) {
+ float vel = getCurrentVelocity(lastX, lastY);
+
+ // We snap back if the current translation is not far enough
+ boolean snapBack = isBelowFalsingThreshold();
+
+ // or if the velocity is in the opposite direction.
+ boolean velIsInWrongDirection = vel * translation < 0;
+ snapBack |= Math.abs(vel) > minFlingVelocity && velIsInWrongDirection;
+ vel = snapBack ^ velIsInWrongDirection ? 0 : vel;
+ fling(vel, snapBack || forceSnapBack, translation < 0);
+ }
+
+ private boolean isBelowFalsingThreshold() {
+ return Math.abs(translation) < Math.abs(translationOnDown) + getMinTranslationAmount();
+ }
+
+ private int getMinTranslationAmount() {
+ float factor = callback.getAffordanceFalsingFactor();
+ return (int) (minTranslationAmount * factor);
+ }
+
+ private void fling(float vel, final boolean snapBack, boolean right) {
+ float target =
+ right ? -callback.getMaxTranslationDistance() : callback.getMaxTranslationDistance();
+ target = snapBack ? 0 : target;
+
+ ValueAnimator animator = ValueAnimator.ofFloat(translation, target);
+ flingAnimationUtils.apply(animator, translation, target, vel);
+ animator.addUpdateListener(
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ translation = (float) animation.getAnimatedValue();
+ }
+ });
+ animator.addListener(flingEndListener);
+ if (!snapBack) {
+ startFinishingCircleAnimation(vel * 0.375f, animationEndRunnable, right);
+ callback.onAnimationToSideStarted(right, translation, vel);
+ } else {
+ reset(true);
+ }
+ animator.start();
+ swipeAnimator = animator;
+ if (snapBack) {
+ callback.onSwipingAborted();
+ }
+ }
+
+ private void startFinishingCircleAnimation(
+ float velocity, Runnable mAnimationEndRunnable, boolean right) {
+ SwipeButtonView targetView = right ? rightIcon : leftIcon;
+ if (targetView != null) {
+ targetView.finishAnimation(velocity, mAnimationEndRunnable);
+ }
+ }
+
+ private void setTranslation(float translation, boolean isReset, boolean animateReset) {
+ translation = rightSwipePossible() ? translation : Math.max(0, translation);
+ translation = leftSwipePossible() ? translation : Math.min(0, translation);
+ float absTranslation = Math.abs(translation);
+ if (translation != this.translation || isReset) {
+ SwipeButtonView targetView = translation > 0 ? leftIcon : rightIcon;
+ SwipeButtonView otherView = translation > 0 ? rightIcon : leftIcon;
+ float alpha = absTranslation / getMinTranslationAmount();
+
+ // We interpolate the alpha of the other icons to 0
+ float fadeOutAlpha = 1.0f - alpha;
+ fadeOutAlpha = Math.max(fadeOutAlpha, 0.0f);
+
+ boolean animateIcons = isReset && animateReset;
+ boolean forceNoCircleAnimation = isReset && !animateReset;
+ float radius = getRadiusFromTranslation(absTranslation);
+ boolean slowAnimation = isReset && isBelowFalsingThreshold();
+ if (targetView != null) {
+ if (!isReset) {
+ updateIcon(
+ targetView,
+ radius,
+ alpha + fadeOutAlpha * targetView.getRestingAlpha(),
+ false,
+ false,
+ false,
+ false);
+ } else {
+ updateIcon(
+ targetView,
+ 0.0f,
+ fadeOutAlpha * targetView.getRestingAlpha(),
+ animateIcons,
+ slowAnimation,
+ false,
+ forceNoCircleAnimation);
+ }
+ }
+ if (otherView != null) {
+ updateIcon(
+ otherView,
+ 0.0f,
+ fadeOutAlpha * otherView.getRestingAlpha(),
+ animateIcons,
+ slowAnimation,
+ false,
+ forceNoCircleAnimation);
+ }
+
+ this.translation = translation;
+ }
+ }
+
+ private void updateIconsFromTranslation(SwipeButtonView targetView) {
+ float absTranslation = Math.abs(translation);
+ float alpha = absTranslation / getMinTranslationAmount();
+
+ // We interpolate the alpha of the other icons to 0
+ float fadeOutAlpha = 1.0f - alpha;
+ fadeOutAlpha = Math.max(0.0f, fadeOutAlpha);
+
+ // We interpolate the alpha of the targetView to 1
+ SwipeButtonView otherView = targetView == rightIcon ? leftIcon : rightIcon;
+ updateIconAlpha(targetView, alpha + fadeOutAlpha * targetView.getRestingAlpha(), false);
+ if (otherView != null) {
+ updateIconAlpha(otherView, fadeOutAlpha * otherView.getRestingAlpha(), false);
+ }
+ }
+
+ private float getTranslationFromRadius(float circleSize) {
+ float translation = (circleSize - minBackgroundRadius) / BACKGROUND_RADIUS_SCALE_FACTOR;
+ return translation > 0.0f ? translation + touchSlop : 0.0f;
+ }
+
+ private float getRadiusFromTranslation(float translation) {
+ if (translation <= touchSlop) {
+ return 0.0f;
+ }
+ return (translation - touchSlop) * BACKGROUND_RADIUS_SCALE_FACTOR + minBackgroundRadius;
+ }
+
+ public void animateHideLeftRightIcon() {
+ cancelAnimation();
+ updateIcon(rightIcon, 0f, 0f, true, false, false, false);
+ updateIcon(leftIcon, 0f, 0f, true, false, false, false);
+ }
+
+ private void updateIcon(
+ @Nullable SwipeButtonView view,
+ float circleRadius,
+ float alpha,
+ boolean animate,
+ boolean slowRadiusAnimation,
+ boolean force,
+ boolean forceNoCircleAnimation) {
+ if (view == null) {
+ return;
+ }
+ if (view.getVisibility() != View.VISIBLE && !force) {
+ return;
+ }
+ if (forceNoCircleAnimation) {
+ view.setCircleRadiusWithoutAnimation(circleRadius);
+ } else {
+ view.setCircleRadius(circleRadius, slowRadiusAnimation);
+ }
+ updateIconAlpha(view, alpha, animate);
+ }
+
+ private void updateIconAlpha(SwipeButtonView view, float alpha, boolean animate) {
+ float scale = getScale(alpha, view);
+ alpha = Math.min(1.0f, alpha);
+ view.setImageAlpha(alpha, animate);
+ view.setImageScale(scale, animate);
+ }
+
+ private float getScale(float alpha, SwipeButtonView icon) {
+ float scale = alpha / icon.getRestingAlpha() * 0.2f + SwipeButtonView.MIN_ICON_SCALE_AMOUNT;
+ return Math.min(scale, SwipeButtonView.MAX_ICON_SCALE_AMOUNT);
+ }
+
+ private void trackMovement(MotionEvent event) {
+ if (velocityTracker != null) {
+ velocityTracker.addMovement(event);
+ }
+ }
+
+ private void initVelocityTracker() {
+ if (velocityTracker != null) {
+ velocityTracker.recycle();
+ }
+ velocityTracker = VelocityTracker.obtain();
+ }
+
+ private float getCurrentVelocity(float lastX, float lastY) {
+ if (velocityTracker == null) {
+ return 0;
+ }
+ velocityTracker.computeCurrentVelocity(1000);
+ float aX = velocityTracker.getXVelocity();
+ float aY = velocityTracker.getYVelocity();
+ float bX = lastX - initialTouchX;
+ float bY = lastY - initialTouchY;
+ float bLen = (float) Math.hypot(bX, bY);
+ // Project the velocity onto the distance vector: a * b / |b|
+ float projectedVelocity = (aX * bX + aY * bY) / bLen;
+ if (targetedView == rightIcon) {
+ projectedVelocity = -projectedVelocity;
+ }
+ return projectedVelocity;
+ }
+
+ public void onConfigurationChanged() {
+ initDimens();
+ initIcons();
+ }
+
+ public void onRtlPropertiesChanged() {
+ initIcons();
+ }
+
+ public void reset(boolean animate) {
+ cancelAnimation();
+ setTranslation(0.0f, true, animate);
+ motionCancelled = true;
+ if (swipingInProgress) {
+ callback.onSwipingAborted();
+ swipingInProgress = false;
+ }
+ }
+
+ public boolean isSwipingInProgress() {
+ return swipingInProgress;
+ }
+
+ public void launchAffordance(boolean animate, boolean left) {
+ SwipeButtonView targetView = left ? leftIcon : rightIcon;
+ if (swipingInProgress || targetView == null) {
+ // We don't want to mess with the state if the user is actually swiping already.
+ return;
+ }
+ SwipeButtonView otherView = left ? rightIcon : leftIcon;
+ startSwiping(targetView);
+ if (animate) {
+ fling(0, false, !left);
+ updateIcon(otherView, 0.0f, 0, true, false, true, false);
+ } else {
+ callback.onAnimationToSideStarted(!left, translation, 0);
+ translation =
+ left ? callback.getMaxTranslationDistance() : callback.getMaxTranslationDistance();
+ updateIcon(otherView, 0.0f, 0.0f, false, false, true, false);
+ targetView.instantFinishAnimation();
+ flingEndListener.onAnimationEnd(null);
+ animationEndRunnable.run();
+ }
+ }
+
+ /** Callback interface for various actions */
+ public interface Callback {
+
+ /**
+ * Notifies the callback when an animation to a side page was started.
+ *
+ * @param rightPage Is the page animated to the right page?
+ */
+ void onAnimationToSideStarted(boolean rightPage, float translation, float vel);
+
+ /** Notifies the callback the animation to a side page has ended. */
+ void onAnimationToSideEnded();
+
+ float getMaxTranslationDistance();
+
+ void onSwipingStarted(boolean rightIcon);
+
+ void onSwipingAborted();
+
+ void onIconClicked(boolean rightIcon);
+
+ @Nullable
+ SwipeButtonView getLeftIcon();
+
+ @Nullable
+ SwipeButtonView getRightIcon();
+
+ @Nullable
+ View getLeftPreview();
+
+ @Nullable
+ View getRightPreview();
+
+ /** @return The factor the minimum swipe amount should be multiplied with. */
+ float getAffordanceFalsingFactor();
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/affordance/SwipeButtonView.java b/java/com/android/incallui/answer/impl/affordance/SwipeButtonView.java
new file mode 100644
index 000000000..46879ea3f
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/affordance/SwipeButtonView.java
@@ -0,0 +1,505 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.affordance;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ArgbEvaluator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewAnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.ImageView;
+import com.android.incallui.answer.impl.utils.FlingAnimationUtils;
+import com.android.incallui.answer.impl.utils.Interpolators;
+
+/** Button that allows swiping to trigger */
+public class SwipeButtonView extends ImageView {
+
+ private static final long CIRCLE_APPEAR_DURATION = 80;
+ private static final long CIRCLE_DISAPPEAR_MAX_DURATION = 200;
+ private static final long NORMAL_ANIMATION_DURATION = 200;
+ public static final float MAX_ICON_SCALE_AMOUNT = 1.5f;
+ public static final float MIN_ICON_SCALE_AMOUNT = 0.8f;
+
+ private final int minBackgroundRadius;
+ private final Paint circlePaint;
+ private final int inverseColor;
+ private final int normalColor;
+ private final ArgbEvaluator colorInterpolator;
+ private final FlingAnimationUtils flingAnimationUtils;
+ private float circleRadius;
+ private int centerX;
+ private int centerY;
+ private ValueAnimator circleAnimator;
+ private ValueAnimator alphaAnimator;
+ private ValueAnimator scaleAnimator;
+ private float circleStartValue;
+ private boolean circleWillBeHidden;
+ private int[] tempPoint = new int[2];
+ private float tmageScale = 1f;
+ private int circleColor;
+ private View previewView;
+ private float circleStartRadius;
+ private float maxCircleSize;
+ private Animator previewClipper;
+ private float restingAlpha = SwipeButtonHelper.SWIPE_RESTING_ALPHA_AMOUNT;
+ private boolean finishing;
+ private boolean launchingAffordance;
+
+ private AnimatorListenerAdapter clipEndListener =
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ previewClipper = null;
+ }
+ };
+ private AnimatorListenerAdapter circleEndListener =
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ circleAnimator = null;
+ }
+ };
+ private AnimatorListenerAdapter scaleEndListener =
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ scaleAnimator = null;
+ }
+ };
+ private AnimatorListenerAdapter alphaEndListener =
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ alphaAnimator = null;
+ }
+ };
+
+ public SwipeButtonView(Context context) {
+ this(context, null);
+ }
+
+ public SwipeButtonView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SwipeButtonView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public SwipeButtonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ circlePaint = new Paint();
+ circlePaint.setAntiAlias(true);
+ circleColor = 0xffffffff;
+ circlePaint.setColor(circleColor);
+
+ normalColor = 0xffffffff;
+ inverseColor = 0xff000000;
+ minBackgroundRadius =
+ context
+ .getResources()
+ .getDimensionPixelSize(R.dimen.answer_affordance_min_background_radius);
+ colorInterpolator = new ArgbEvaluator();
+ flingAnimationUtils = new FlingAnimationUtils(context, 0.3f);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ centerX = getWidth() / 2;
+ centerY = getHeight() / 2;
+ maxCircleSize = getMaxCircleSize();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ drawBackgroundCircle(canvas);
+ canvas.save();
+ canvas.scale(tmageScale, tmageScale, getWidth() / 2, getHeight() / 2);
+ super.onDraw(canvas);
+ canvas.restore();
+ }
+
+ public void setPreviewView(@Nullable View v) {
+ View oldPreviewView = previewView;
+ previewView = v;
+ if (previewView != null) {
+ previewView.setVisibility(launchingAffordance ? oldPreviewView.getVisibility() : INVISIBLE);
+ }
+ }
+
+ private void updateIconColor() {
+ Drawable drawable = getDrawable().mutate();
+ float alpha = circleRadius / minBackgroundRadius;
+ alpha = Math.min(1.0f, alpha);
+ int color = (int) colorInterpolator.evaluate(alpha, normalColor, inverseColor);
+ drawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
+ }
+
+ private void drawBackgroundCircle(Canvas canvas) {
+ if (circleRadius > 0 || finishing) {
+ updateCircleColor();
+ canvas.drawCircle(centerX, centerY, circleRadius, circlePaint);
+ }
+ }
+
+ private void updateCircleColor() {
+ float fraction =
+ 0.5f
+ + 0.5f
+ * Math.max(
+ 0.0f,
+ Math.min(
+ 1.0f, (circleRadius - minBackgroundRadius) / (0.5f * minBackgroundRadius)));
+ if (previewView != null && previewView.getVisibility() == VISIBLE) {
+ float finishingFraction =
+ 1 - Math.max(0, circleRadius - circleStartRadius) / (maxCircleSize - circleStartRadius);
+ fraction *= finishingFraction;
+ }
+ int color =
+ Color.argb(
+ (int) (Color.alpha(circleColor) * fraction),
+ Color.red(circleColor),
+ Color.green(circleColor),
+ Color.blue(circleColor));
+ circlePaint.setColor(color);
+ }
+
+ public void finishAnimation(float velocity, @Nullable final Runnable mAnimationEndRunnable) {
+ cancelAnimator(circleAnimator);
+ cancelAnimator(previewClipper);
+ finishing = true;
+ circleStartRadius = circleRadius;
+ final float maxCircleSize = getMaxCircleSize();
+ Animator animatorToRadius;
+ animatorToRadius = getAnimatorToRadius(maxCircleSize);
+ flingAnimationUtils.applyDismissing(
+ animatorToRadius, circleRadius, maxCircleSize, velocity, maxCircleSize);
+ animatorToRadius.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mAnimationEndRunnable != null) {
+ mAnimationEndRunnable.run();
+ }
+ finishing = false;
+ circleRadius = maxCircleSize;
+ invalidate();
+ }
+ });
+ animatorToRadius.start();
+ setImageAlpha(0, true);
+ if (previewView != null) {
+ previewView.setVisibility(View.VISIBLE);
+ previewClipper =
+ ViewAnimationUtils.createCircularReveal(
+ previewView, getLeft() + centerX, getTop() + centerY, circleRadius, maxCircleSize);
+ flingAnimationUtils.applyDismissing(
+ previewClipper, circleRadius, maxCircleSize, velocity, maxCircleSize);
+ previewClipper.addListener(clipEndListener);
+ previewClipper.start();
+ }
+ }
+
+ public void instantFinishAnimation() {
+ cancelAnimator(previewClipper);
+ if (previewView != null) {
+ previewView.setClipBounds(null);
+ previewView.setVisibility(View.VISIBLE);
+ }
+ circleRadius = getMaxCircleSize();
+ setImageAlpha(0, false);
+ invalidate();
+ }
+
+ private float getMaxCircleSize() {
+ getLocationInWindow(tempPoint);
+ float rootWidth = getRootView().getWidth();
+ float width = tempPoint[0] + centerX;
+ width = Math.max(rootWidth - width, width);
+ float height = tempPoint[1] + centerY;
+ return (float) Math.hypot(width, height);
+ }
+
+ public void setCircleRadius(float circleRadius) {
+ setCircleRadius(circleRadius, false, false);
+ }
+
+ public void setCircleRadius(float circleRadius, boolean slowAnimation) {
+ setCircleRadius(circleRadius, slowAnimation, false);
+ }
+
+ public void setCircleRadiusWithoutAnimation(float circleRadius) {
+ cancelAnimator(circleAnimator);
+ setCircleRadius(circleRadius, false, true);
+ }
+
+ private void setCircleRadius(float circleRadius, boolean slowAnimation, boolean noAnimation) {
+
+ // Check if we need a new animation
+ boolean radiusHidden =
+ (circleAnimator != null && circleWillBeHidden)
+ || (circleAnimator == null && this.circleRadius == 0.0f);
+ boolean nowHidden = circleRadius == 0.0f;
+ boolean radiusNeedsAnimation = (radiusHidden != nowHidden) && !noAnimation;
+ if (!radiusNeedsAnimation) {
+ if (circleAnimator == null) {
+ this.circleRadius = circleRadius;
+ updateIconColor();
+ invalidate();
+ if (nowHidden) {
+ if (previewView != null) {
+ previewView.setVisibility(View.INVISIBLE);
+ }
+ }
+ } else if (!circleWillBeHidden) {
+
+ // We just update the end value
+ float diff = circleRadius - minBackgroundRadius;
+ PropertyValuesHolder[] values = circleAnimator.getValues();
+ values[0].setFloatValues(circleStartValue + diff, circleRadius);
+ circleAnimator.setCurrentPlayTime(circleAnimator.getCurrentPlayTime());
+ }
+ } else {
+ cancelAnimator(circleAnimator);
+ cancelAnimator(previewClipper);
+ ValueAnimator animator = getAnimatorToRadius(circleRadius);
+ Interpolator interpolator =
+ circleRadius == 0.0f
+ ? Interpolators.FAST_OUT_LINEAR_IN
+ : Interpolators.LINEAR_OUT_SLOW_IN;
+ animator.setInterpolator(interpolator);
+ long duration = 250;
+ if (!slowAnimation) {
+ float durationFactor =
+ Math.abs(this.circleRadius - circleRadius) / (float) minBackgroundRadius;
+ duration = (long) (CIRCLE_APPEAR_DURATION * durationFactor);
+ duration = Math.min(duration, CIRCLE_DISAPPEAR_MAX_DURATION);
+ }
+ animator.setDuration(duration);
+ animator.start();
+ if (previewView != null && previewView.getVisibility() == View.VISIBLE) {
+ previewView.setVisibility(View.VISIBLE);
+ previewClipper =
+ ViewAnimationUtils.createCircularReveal(
+ previewView,
+ getLeft() + centerX,
+ getTop() + centerY,
+ this.circleRadius,
+ circleRadius);
+ previewClipper.setInterpolator(interpolator);
+ previewClipper.setDuration(duration);
+ previewClipper.addListener(clipEndListener);
+ previewClipper.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ previewView.setVisibility(View.INVISIBLE);
+ }
+ });
+ previewClipper.start();
+ }
+ }
+ }
+
+ private ValueAnimator getAnimatorToRadius(float circleRadius) {
+ ValueAnimator animator = ValueAnimator.ofFloat(this.circleRadius, circleRadius);
+ circleAnimator = animator;
+ circleStartValue = this.circleRadius;
+ circleWillBeHidden = circleRadius == 0.0f;
+ animator.addUpdateListener(
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ SwipeButtonView.this.circleRadius = (float) animation.getAnimatedValue();
+ updateIconColor();
+ invalidate();
+ }
+ });
+ animator.addListener(circleEndListener);
+ return animator;
+ }
+
+ private void cancelAnimator(Animator animator) {
+ if (animator != null) {
+ animator.cancel();
+ }
+ }
+
+ public void setImageScale(float imageScale, boolean animate) {
+ setImageScale(imageScale, animate, -1, null);
+ }
+
+ /**
+ * Sets the scale of the containing image
+ *
+ * @param imageScale The new Scale.
+ * @param animate Should an animation be performed
+ * @param duration If animate, whats the duration? When -1 we take the default duration
+ * @param interpolator If animate, whats the interpolator? When null we take the default
+ * interpolator.
+ */
+ public void setImageScale(
+ float imageScale, boolean animate, long duration, @Nullable Interpolator interpolator) {
+ cancelAnimator(scaleAnimator);
+ if (!animate) {
+ tmageScale = imageScale;
+ invalidate();
+ } else {
+ ValueAnimator animator = ValueAnimator.ofFloat(tmageScale, imageScale);
+ scaleAnimator = animator;
+ animator.addUpdateListener(
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ tmageScale = (float) animation.getAnimatedValue();
+ invalidate();
+ }
+ });
+ animator.addListener(scaleEndListener);
+ if (interpolator == null) {
+ interpolator =
+ imageScale == 0.0f
+ ? Interpolators.FAST_OUT_LINEAR_IN
+ : Interpolators.LINEAR_OUT_SLOW_IN;
+ }
+ animator.setInterpolator(interpolator);
+ if (duration == -1) {
+ float durationFactor = Math.abs(tmageScale - imageScale) / (1.0f - MIN_ICON_SCALE_AMOUNT);
+ durationFactor = Math.min(1.0f, durationFactor);
+ duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor);
+ }
+ animator.setDuration(duration);
+ animator.start();
+ }
+ }
+
+ public void setRestingAlpha(float alpha) {
+ restingAlpha = alpha;
+
+ // TODO: Handle the case an animation is playing.
+ setImageAlpha(alpha, false);
+ }
+
+ public float getRestingAlpha() {
+ return restingAlpha;
+ }
+
+ public void setImageAlpha(float alpha, boolean animate) {
+ setImageAlpha(alpha, animate, -1, null, null);
+ }
+
+ /**
+ * Sets the alpha of the containing image
+ *
+ * @param alpha The new alpha.
+ * @param animate Should an animation be performed
+ * @param duration If animate, whats the duration? When -1 we take the default duration
+ * @param interpolator If animate, whats the interpolator? When null we take the default
+ * interpolator.
+ */
+ public void setImageAlpha(
+ float alpha,
+ boolean animate,
+ long duration,
+ @Nullable Interpolator interpolator,
+ @Nullable Runnable runnable) {
+ cancelAnimator(alphaAnimator);
+ alpha = launchingAffordance ? 0 : alpha;
+ int endAlpha = (int) (alpha * 255);
+ final Drawable background = getBackground();
+ if (!animate) {
+ if (background != null) {
+ background.mutate().setAlpha(endAlpha);
+ }
+ setImageAlpha(endAlpha);
+ } else {
+ int currentAlpha = getImageAlpha();
+ ValueAnimator animator = ValueAnimator.ofInt(currentAlpha, endAlpha);
+ alphaAnimator = animator;
+ animator.addUpdateListener(
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ int alpha = (int) animation.getAnimatedValue();
+ if (background != null) {
+ background.mutate().setAlpha(alpha);
+ }
+ setImageAlpha(alpha);
+ }
+ });
+ animator.addListener(alphaEndListener);
+ if (interpolator == null) {
+ interpolator =
+ alpha == 0.0f ? Interpolators.FAST_OUT_LINEAR_IN : Interpolators.LINEAR_OUT_SLOW_IN;
+ }
+ animator.setInterpolator(interpolator);
+ if (duration == -1) {
+ float durationFactor = Math.abs(currentAlpha - endAlpha) / 255f;
+ durationFactor = Math.min(1.0f, durationFactor);
+ duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor);
+ }
+ animator.setDuration(duration);
+ if (runnable != null) {
+ animator.addListener(getEndListener(runnable));
+ }
+ animator.start();
+ }
+ }
+
+ private Animator.AnimatorListener getEndListener(final Runnable runnable) {
+ return new AnimatorListenerAdapter() {
+ boolean mCancelled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!mCancelled) {
+ runnable.run();
+ }
+ }
+ };
+ }
+
+ public float getCircleRadius() {
+ return circleRadius;
+ }
+
+ @Override
+ public boolean performClick() {
+ return isClickable() && super.performClick();
+ }
+
+ public void setLaunchingAffordance(boolean launchingAffordance) {
+ this.launchingAffordance = launchingAffordance;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/affordance/res/values/dimens.xml b/java/com/android/incallui/answer/impl/affordance/res/values/dimens.xml
new file mode 100644
index 000000000..71d014dd9
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/affordance/res/values/dimens.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <dimen name="answer_min_swipe_amount">110dp</dimen>
+ <dimen name="answer_affordance_min_background_radius">30dp</dimen>
+ <dimen name="answer_affordance_touch_target_size">120dp</dimen>
+ <dimen name="hint_grow_amount_sideways">60dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/answermethod/AndroidManifest.xml b/java/com/android/incallui/answer/impl/answermethod/AndroidManifest.xml
new file mode 100644
index 000000000..9082407f1
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.incallui.answer.impl.answermethod">
+</manifest>
diff --git a/java/com/android/incallui/answer/impl/answermethod/AnswerMethod.java b/java/com/android/incallui/answer/impl/answermethod/AnswerMethod.java
new file mode 100644
index 000000000..5efd3f05b
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/AnswerMethod.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.answermethod;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import com.android.dialer.common.FragmentUtils;
+
+/** A fragment that can be used to answer/reject calls. */
+public abstract class AnswerMethod extends Fragment {
+
+ public abstract void setHintText(@Nullable CharSequence hintText);
+
+ public abstract void setShowIncomingWillDisconnect(boolean incomingWillDisconnect);
+
+ public void setContactPhoto(@Nullable Drawable contactPhoto) {
+ // default implementation does nothing. Only some AnswerMethods show a photo
+ }
+
+ protected AnswerMethodHolder getParent() {
+ return FragmentUtils.getParentUnsafe(this, AnswerMethodHolder.class);
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ FragmentUtils.checkParent(this, AnswerMethodHolder.class);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/answermethod/AnswerMethodFactory.java b/java/com/android/incallui/answer/impl/answermethod/AnswerMethodFactory.java
new file mode 100644
index 000000000..35f36f727
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/AnswerMethodFactory.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.answermethod;
+
+import android.app.Activity;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import com.android.dialer.compat.ActivityCompat;
+import com.android.incallui.util.AccessibilityUtil;
+
+/** Creates the appropriate {@link AnswerMethod} for the circumstances. */
+public class AnswerMethodFactory {
+
+ @NonNull
+ public static AnswerMethod createAnswerMethod(@NonNull Activity activity) {
+ if (needTwoButton(activity)) {
+ return new TwoButtonMethod();
+ } else {
+ return new FlingUpDownMethod();
+ }
+ }
+
+ public static boolean needsReplacement(@Nullable Fragment answerMethod) {
+ //noinspection SimplifiableIfStatement
+ if (answerMethod == null) {
+ return true;
+ }
+ // If we have already started showing TwoButtonMethod, we should keep showing TwoButtonMethod.
+ // Otherwise check if we need to change to TwoButtonMethod
+ return !(answerMethod instanceof TwoButtonMethod) && needTwoButton(answerMethod.getActivity());
+ }
+
+ private static boolean needTwoButton(@NonNull Activity activity) {
+ return AccessibilityUtil.isTouchExplorationEnabled(activity)
+ || ActivityCompat.isInMultiWindowMode(activity);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java b/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java
new file mode 100644
index 000000000..4052281b7
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.answermethod;
+
+import android.support.annotation.FloatRange;
+
+/** Defines callbacks {@link AnswerMethod AnswerMethods} may use to update their parent. */
+public interface AnswerMethodHolder {
+
+ /**
+ * Update animation based on method progress.
+ *
+ * @param answerProgress float representing progress. -1 is fully declined, 1 is fully answered,
+ * and 0 is neutral.
+ */
+ void onAnswerProgressUpdate(@FloatRange(from = -1f, to = 1f) float answerProgress);
+
+ /** Answer the current call. */
+ void answerFromMethod();
+
+ /** Reject the current call. */
+ void rejectFromMethod();
+
+ /** Set AnswerProgress to zero (not due to normal updates). */
+ void resetAnswerProgress();
+
+ /**
+ * Check whether the current call is a video call.
+ *
+ * @return true iff the current call is a video call.
+ */
+ boolean isVideoCall();
+}
diff --git a/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java
new file mode 100644
index 000000000..0bc65818c
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java
@@ -0,0 +1,1149 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.answermethod;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.annotation.ColorInt;
+import android.support.annotation.FloatRange;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.graphics.ColorUtils;
+import android.support.v4.view.animation.FastOutLinearInInterpolator;
+import android.support.v4.view.animation.FastOutSlowInInterpolator;
+import android.support.v4.view.animation.LinearOutSlowInInterpolator;
+import android.support.v4.view.animation.PathInterpolatorCompat;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.AccessibilityDelegate;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.animation.BounceInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.android.dialer.common.DpUtil;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.MathUtil;
+import com.android.dialer.util.DrawableConverter;
+import com.android.dialer.util.ViewUtil;
+import com.android.incallui.answer.impl.answermethod.FlingUpDownTouchHandler.OnProgressChangedListener;
+import com.android.incallui.answer.impl.classifier.FalsingManager;
+import com.android.incallui.answer.impl.hint.AnswerHint;
+import com.android.incallui.answer.impl.hint.AnswerHintFactory;
+import com.android.incallui.answer.impl.hint.EventPayloadLoaderImpl;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Answer method that swipes up to answer or down to reject. */
+@SuppressLint("ClickableViewAccessibility")
+public class FlingUpDownMethod extends AnswerMethod implements OnProgressChangedListener {
+
+ private static final float SWIPE_LERP_PROGRESS_FACTOR = 0.5f;
+ private static final long ANIMATE_DURATION_SHORT_MILLIS = 667;
+ private static final long ANIMATE_DURATION_NORMAL_MILLIS = 1_333;
+ private static final long ANIMATE_DURATION_LONG_MILLIS = 1_500;
+ private static final long BOUNCE_ANIMATION_DELAY = 167;
+ private static final long VIBRATION_TIME_MILLIS = 1_833;
+ private static final long SETTLE_ANIMATION_DURATION_MILLIS = 100;
+ private static final int HINT_JUMP_DP = 60;
+ private static final int HINT_DIP_DP = 8;
+ private static final float HINT_SCALE_RATIO = 1.15f;
+ private static final long SWIPE_TO_DECLINE_FADE_IN_DELAY_MILLIS = 333;
+ private static final int HINT_REJECT_SHOW_DURATION_MILLIS = 2000;
+ private static final int ICON_END_CALL_ROTATION_DEGREES = 135;
+ private static final int HINT_REJECT_FADE_TRANSLATION_Y_DP = -8;
+ private static final float SWIPE_TO_ANSWER_MAX_TRANSLATION_Y_DP = 150;
+ private static final int SWIPE_TO_REJECT_MAX_TRANSLATION_Y_DP = 24;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ AnimationState.NONE,
+ AnimationState.ENTRY,
+ AnimationState.BOUNCE,
+ AnimationState.SWIPE,
+ AnimationState.SETTLE,
+ AnimationState.HINT,
+ AnimationState.COMPLETED
+ }
+ )
+ @VisibleForTesting
+ @interface AnimationState {
+
+ int NONE = 0;
+ int ENTRY = 1; // Entry animation for incoming call
+ int BOUNCE = 2; // An idle state in which text and icon slightly bounces off its base repeatedly
+ int SWIPE = 3; // A special state in which text and icon follows the finger movement
+ int SETTLE = 4; // A short animation to reset from swipe and prepare for hint or bounce
+ int HINT = 5; // Jump animation to suggest what to do
+ int COMPLETED = 6; // Animation loop completed. Occurs after user swipes beyond threshold
+ }
+
+ private static void moveTowardY(View view, float newY) {
+ view.setTranslationY(MathUtil.lerp(view.getTranslationY(), newY, SWIPE_LERP_PROGRESS_FACTOR));
+ }
+
+ private static void moveTowardX(View view, float newX) {
+ view.setTranslationX(MathUtil.lerp(view.getTranslationX(), newX, SWIPE_LERP_PROGRESS_FACTOR));
+ }
+
+ private static void fadeToward(View view, float newAlpha) {
+ view.setAlpha(MathUtil.lerp(view.getAlpha(), newAlpha, SWIPE_LERP_PROGRESS_FACTOR));
+ }
+
+ private static void rotateToward(View view, float newRotation) {
+ view.setRotation(MathUtil.lerp(view.getRotation(), newRotation, SWIPE_LERP_PROGRESS_FACTOR));
+ }
+
+ private TextView swipeToAnswerText;
+ private TextView swipeToRejectText;
+ private View contactPuckContainer;
+ private ImageView contactPuckBackground;
+ private ImageView contactPuckIcon;
+ private View incomingDisconnectText;
+ private Animator lockBounceAnim;
+ private AnimatorSet lockEntryAnim;
+ private AnimatorSet lockHintAnim;
+ private AnimatorSet lockSettleAnim;
+ @AnimationState private int animationState = AnimationState.NONE;
+ @AnimationState private int afterSettleAnimationState = AnimationState.NONE;
+ // a value for finger swipe progress. -1 or less for "reject"; 1 or more for "accept".
+ private float swipeProgress;
+ private Animator rejectHintHide;
+ private Animator vibrationAnimator;
+ private Drawable contactPhoto;
+ private boolean incomingWillDisconnect;
+ private FlingUpDownTouchHandler touchHandler;
+ private FalsingManager falsingManager;
+
+ private AnswerHint answerHint;
+
+ @Override
+ public void onCreate(@Nullable Bundle bundle) {
+ super.onCreate(bundle);
+ falsingManager = new FalsingManager(getContext());
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ falsingManager.onScreenOn();
+ if (getView() != null) {
+ if (animationState == AnimationState.SWIPE || animationState == AnimationState.HINT) {
+ swipeProgress = 0;
+ updateContactPuck();
+ onMoveReset(false);
+ } else if (animationState == AnimationState.ENTRY) {
+ // When starting from the lock screen, the activity may be stopped and started briefly.
+ // Don't let that interrupt the entry animation
+ startSwipeToAnswerEntryAnimation();
+ }
+ }
+ }
+
+ @Override
+ public void onStop() {
+ endAnimation();
+ falsingManager.onScreenOff();
+ if (getActivity().isFinishing()) {
+ setAnimationState(AnimationState.COMPLETED);
+ }
+ super.onStop();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+ View view = layoutInflater.inflate(R.layout.swipe_up_down_method, viewGroup, false);
+
+ contactPuckContainer = view.findViewById(R.id.incoming_call_puck_container);
+ contactPuckBackground = (ImageView) view.findViewById(R.id.incoming_call_puck_bg);
+ contactPuckIcon = (ImageView) view.findViewById(R.id.incoming_call_puck_icon);
+ swipeToAnswerText = (TextView) view.findViewById(R.id.incoming_swipe_to_answer_text);
+ swipeToRejectText = (TextView) view.findViewById(R.id.incoming_swipe_to_reject_text);
+ incomingDisconnectText = view.findViewById(R.id.incoming_will_disconnect_text);
+ incomingDisconnectText.setAlpha(incomingWillDisconnect ? 1 : 0);
+
+ view.setAccessibilityDelegate(
+ new AccessibilityDelegate() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ info.addAction(
+ new AccessibilityAction(
+ R.id.accessibility_action_answer, getString(R.string.call_incoming_answer)));
+ info.addAction(
+ new AccessibilityAction(
+ R.id.accessibility_action_decline, getString(R.string.call_incoming_decline)));
+ }
+
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ if (action == R.id.accessibility_action_answer) {
+ performAccept();
+ return true;
+ } else if (action == R.id.accessibility_action_decline) {
+ performReject();
+ return true;
+ }
+ return super.performAccessibilityAction(host, action, args);
+ }
+ });
+
+ swipeProgress = 0;
+
+ updateContactPuck();
+
+ touchHandler = FlingUpDownTouchHandler.attach(view, this, falsingManager);
+
+ answerHint =
+ new AnswerHintFactory(new EventPayloadLoaderImpl())
+ .create(getContext(), ANIMATE_DURATION_LONG_MILLIS, BOUNCE_ANIMATION_DELAY);
+ answerHint.onCreateView(
+ layoutInflater,
+ (ViewGroup) view.findViewById(R.id.hint_container),
+ contactPuckContainer,
+ swipeToAnswerText);
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle bundle) {
+ super.onViewCreated(view, bundle);
+ setAnimationState(AnimationState.ENTRY);
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ if (touchHandler != null) {
+ touchHandler.detach();
+ touchHandler = null;
+ }
+ }
+
+ @Override
+ public void onProgressChanged(@FloatRange(from = -1f, to = 1f) float progress) {
+ swipeProgress = progress;
+ if (animationState == AnimationState.SWIPE && getContext() != null && isVisible()) {
+ updateSwipeTextAndPuckForTouch();
+ }
+ }
+
+ @Override
+ public void onTrackingStart() {
+ setAnimationState(AnimationState.SWIPE);
+ }
+
+ @Override
+ public void onTrackingStopped() {}
+
+ @Override
+ public void onMoveReset(boolean showHint) {
+ if (showHint) {
+ showSwipeHint();
+ } else {
+ setAnimationState(AnimationState.BOUNCE);
+ }
+ resetTouchState();
+ getParent().resetAnswerProgress();
+ }
+
+ @Override
+ public void onMoveFinish(boolean accept) {
+ touchHandler.setTouchEnabled(false);
+ answerHint.onAnswered();
+ if (accept) {
+ performAccept();
+ } else {
+ performReject();
+ }
+ }
+
+ @Override
+ public boolean shouldUseFalsing(@NonNull MotionEvent downEvent) {
+ if (contactPuckContainer == null) {
+ return false;
+ }
+
+ float puckCenterX = contactPuckContainer.getX() + (contactPuckContainer.getWidth() / 2);
+ float puckCenterY = contactPuckContainer.getY() + (contactPuckContainer.getHeight() / 2);
+ double radius = contactPuckContainer.getHeight() / 2;
+
+ // Squaring a number is more performant than taking a sqrt, so we compare the square of the
+ // distance with the square of the radius.
+ double distSq =
+ Math.pow(downEvent.getX() - puckCenterX, 2) + Math.pow(downEvent.getY() - puckCenterY, 2);
+ return distSq >= Math.pow(radius, 2);
+ }
+
+ @Override
+ public void setContactPhoto(Drawable contactPhoto) {
+ this.contactPhoto = contactPhoto;
+
+ updateContactPuck();
+ }
+
+ private void updateContactPuck() {
+ if (contactPuckIcon == null) {
+ return;
+ }
+ if (getParent().isVideoCall()) {
+ contactPuckIcon.setImageResource(R.drawable.quantum_ic_videocam_white_24);
+ } else {
+ contactPuckIcon.setImageResource(R.drawable.quantum_ic_call_white_24);
+ }
+
+ int size =
+ contactPuckBackground
+ .getResources()
+ .getDimensionPixelSize(
+ shouldShowPhotoInPuck()
+ ? R.dimen.answer_contact_puck_size_photo
+ : R.dimen.answer_contact_puck_size_no_photo);
+ contactPuckBackground.setImageDrawable(
+ shouldShowPhotoInPuck()
+ ? makeRoundedDrawable(contactPuckBackground.getContext(), contactPhoto, size)
+ : null);
+ ViewGroup.LayoutParams contactPuckParams = contactPuckBackground.getLayoutParams();
+ contactPuckParams.height = size;
+ contactPuckParams.width = size;
+ contactPuckBackground.setLayoutParams(contactPuckParams);
+ contactPuckIcon.setAlpha(shouldShowPhotoInPuck() ? 0f : 1f);
+ }
+
+ private Drawable makeRoundedDrawable(Context context, Drawable contactPhoto, int size) {
+ return DrawableConverter.getRoundedDrawable(context, contactPhoto, size, size);
+ }
+
+ private boolean shouldShowPhotoInPuck() {
+ return getParent().isVideoCall() && contactPhoto != null;
+ }
+
+ @Override
+ public void setHintText(@Nullable CharSequence hintText) {
+ if (hintText == null) {
+ swipeToAnswerText.setText(R.string.call_incoming_swipe_to_answer);
+ swipeToRejectText.setText(R.string.call_incoming_swipe_to_reject);
+ } else {
+ swipeToAnswerText.setText(hintText);
+ swipeToRejectText.setText(null);
+ }
+ }
+
+ @Override
+ public void setShowIncomingWillDisconnect(boolean incomingWillDisconnect) {
+ this.incomingWillDisconnect = incomingWillDisconnect;
+ if (incomingDisconnectText != null) {
+ incomingDisconnectText.animate().alpha(incomingWillDisconnect ? 1 : 0);
+ }
+ }
+
+ private void showSwipeHint() {
+ setAnimationState(AnimationState.HINT);
+ }
+
+ private void updateSwipeTextAndPuckForTouch() {
+ // Clamp progress value between -1 and 1.
+ final float clampedProgress = MathUtil.clamp(swipeProgress, -1 /* min */, 1 /* max */);
+ final float positiveAdjustedProgress = Math.abs(clampedProgress);
+ final boolean isAcceptingFlow = clampedProgress >= 0;
+
+ // Cancel view property animators on views we're about to mutate
+ swipeToAnswerText.animate().cancel();
+ contactPuckIcon.animate().cancel();
+
+ // Since the animation progression is controlled by user gesture instead of real timeline, the
+ // spec timeline can be divided into 9 slots. Each slot is equivalent to 83ms in the spec.
+ // Therefore, we use 9 slots of 83ms to map user gesture into the spec timeline.
+ final float progressSlots = 9;
+
+ // Fade out the "swipe up to answer". It only takes 1 slot to complete the fade.
+ float swipeTextAlpha = Math.max(0, 1 - Math.abs(clampedProgress) * progressSlots);
+ fadeToward(swipeToAnswerText, swipeTextAlpha);
+ // Fade out the "swipe down to dismiss" at the same time. Don't ever increase its alpha
+ fadeToward(swipeToRejectText, Math.min(swipeTextAlpha, swipeToRejectText.getAlpha()));
+ // Fade out the "incoming will disconnect" text
+ fadeToward(incomingDisconnectText, incomingWillDisconnect ? swipeTextAlpha : 0);
+
+ // Move swipe text back to zero.
+ moveTowardX(swipeToAnswerText, 0 /* newX */);
+ moveTowardY(swipeToAnswerText, 0 /* newY */);
+
+ // Animate puck color
+ @ColorInt
+ int destPuckColor =
+ getContext()
+ .getColor(
+ isAcceptingFlow ? R.color.call_accept_background : R.color.call_hangup_background);
+ destPuckColor =
+ ColorUtils.setAlphaComponent(destPuckColor, (int) (0xFF * positiveAdjustedProgress));
+ contactPuckBackground.setBackgroundTintList(ColorStateList.valueOf(destPuckColor));
+ contactPuckBackground.setBackgroundTintMode(Mode.SRC_ATOP);
+ contactPuckBackground.setColorFilter(destPuckColor);
+
+ // Animate decline icon
+ if (isAcceptingFlow || getParent().isVideoCall()) {
+ rotateToward(contactPuckIcon, 0f);
+ } else {
+ rotateToward(contactPuckIcon, positiveAdjustedProgress * ICON_END_CALL_ROTATION_DEGREES);
+ }
+
+ // Fade in icon
+ if (shouldShowPhotoInPuck()) {
+ fadeToward(contactPuckIcon, positiveAdjustedProgress);
+ }
+ float iconProgress = Math.min(1f, positiveAdjustedProgress * 4);
+ @ColorInt
+ int iconColor =
+ ColorUtils.setAlphaComponent(
+ contactPuckIcon.getContext().getColor(R.color.incoming_answer_icon),
+ (int) (0xFF * (1 - iconProgress)));
+ contactPuckIcon.setImageTintList(ColorStateList.valueOf(iconColor));
+
+ // Move puck.
+ if (isAcceptingFlow) {
+ moveTowardY(
+ contactPuckContainer,
+ -clampedProgress * DpUtil.dpToPx(getContext(), SWIPE_TO_ANSWER_MAX_TRANSLATION_Y_DP));
+ } else {
+ moveTowardY(
+ contactPuckContainer,
+ -clampedProgress * DpUtil.dpToPx(getContext(), SWIPE_TO_REJECT_MAX_TRANSLATION_Y_DP));
+ }
+
+ getParent().onAnswerProgressUpdate(clampedProgress);
+ }
+
+ private void startSwipeToAnswerSwipeAnimation() {
+ LogUtil.i("FlingUpDownMethod.startSwipeToAnswerSwipeAnimation", "Start swipe animation.");
+ resetTouchState();
+ endAnimation();
+ }
+
+ private void setPuckTouchState() {
+ contactPuckBackground.setActivated(touchHandler.isTracking());
+ }
+
+ private void resetTouchState() {
+ if (getContext() == null) {
+ // State will be reset in onStart(), so just abort.
+ return;
+ }
+ contactPuckContainer.animate().scaleX(1 /* scaleX */);
+ contactPuckContainer.animate().scaleY(1 /* scaleY */);
+ contactPuckBackground.animate().scaleX(1 /* scaleX */);
+ contactPuckBackground.animate().scaleY(1 /* scaleY */);
+ contactPuckBackground.setBackgroundTintList(null);
+ contactPuckBackground.setColorFilter(null);
+ contactPuckIcon.setImageTintList(
+ ColorStateList.valueOf(getContext().getColor(R.color.incoming_answer_icon)));
+ contactPuckIcon.animate().rotation(0);
+
+ getParent().resetAnswerProgress();
+ setPuckTouchState();
+
+ final float alpha = 1;
+ swipeToAnswerText.animate().alpha(alpha);
+ contactPuckContainer.animate().alpha(alpha);
+ contactPuckBackground.animate().alpha(alpha);
+ contactPuckIcon.animate().alpha(shouldShowPhotoInPuck() ? 0 : alpha);
+ }
+
+ @VisibleForTesting
+ void setAnimationState(@AnimationState int state) {
+ if (state != AnimationState.HINT && animationState == state) {
+ return;
+ }
+
+ if (animationState == AnimationState.COMPLETED) {
+ LogUtil.e(
+ "FlingUpDownMethod.setAnimationState",
+ "Animation loop has completed. Cannot switch to new state: " + state);
+ return;
+ }
+
+ if (state == AnimationState.HINT || state == AnimationState.BOUNCE) {
+ if (animationState == AnimationState.SWIPE) {
+ afterSettleAnimationState = state;
+ state = AnimationState.SETTLE;
+ }
+ }
+
+ LogUtil.i("FlingUpDownMethod.setAnimationState", "animation state: " + state);
+ animationState = state;
+
+ // Start animation after the current one is finished completely.
+ View view = getView();
+ if (view != null) {
+ // As long as the fragment is added, we can start update the animation state.
+ if (isAdded() && (animationState == state)) {
+ updateAnimationState();
+ } else {
+ endAnimation();
+ }
+ }
+ }
+
+ @AnimationState
+ @VisibleForTesting
+ int getAnimationState() {
+ return animationState;
+ }
+
+ private void updateAnimationState() {
+ switch (animationState) {
+ case AnimationState.ENTRY:
+ startSwipeToAnswerEntryAnimation();
+ break;
+ case AnimationState.BOUNCE:
+ startSwipeToAnswerBounceAnimation();
+ break;
+ case AnimationState.SWIPE:
+ startSwipeToAnswerSwipeAnimation();
+ break;
+ case AnimationState.SETTLE:
+ startSwipeToAnswerSettleAnimation();
+ break;
+ case AnimationState.COMPLETED:
+ clearSwipeToAnswerUi();
+ break;
+ case AnimationState.HINT:
+ startSwipeToAnswerHintAnimation();
+ break;
+ case AnimationState.NONE:
+ default:
+ LogUtil.e(
+ "FlingUpDownMethod.updateAnimationState",
+ "Unexpected animation state: " + animationState);
+ break;
+ }
+ }
+
+ private void startSwipeToAnswerEntryAnimation() {
+ LogUtil.i("FlingUpDownMethod.startSwipeToAnswerEntryAnimation", "Swipe entry animation.");
+ endAnimation();
+
+ lockEntryAnim = new AnimatorSet();
+ Animator textUp =
+ ObjectAnimator.ofFloat(
+ swipeToAnswerText,
+ View.TRANSLATION_Y,
+ DpUtil.dpToPx(getContext(), 192 /* dp */),
+ DpUtil.dpToPx(getContext(), -20 /* dp */));
+ textUp.setDuration(ANIMATE_DURATION_NORMAL_MILLIS);
+ textUp.setInterpolator(new LinearOutSlowInInterpolator());
+
+ Animator textDown =
+ ObjectAnimator.ofFloat(
+ swipeToAnswerText,
+ View.TRANSLATION_Y,
+ DpUtil.dpToPx(getContext(), -20) /* dp */,
+ 0 /* end pos */);
+ textDown.setDuration(ANIMATE_DURATION_NORMAL_MILLIS);
+ textUp.setInterpolator(new FastOutSlowInInterpolator());
+
+ // "Swipe down to reject" text fades in with a slight translation
+ swipeToRejectText.setAlpha(0f);
+ Animator rejectTextShow =
+ ObjectAnimator.ofPropertyValuesHolder(
+ swipeToRejectText,
+ PropertyValuesHolder.ofFloat(View.ALPHA, 1f),
+ PropertyValuesHolder.ofFloat(
+ View.TRANSLATION_Y,
+ DpUtil.dpToPx(getContext(), HINT_REJECT_FADE_TRANSLATION_Y_DP),
+ 0f));
+ rejectTextShow.setInterpolator(new FastOutLinearInInterpolator());
+ rejectTextShow.setDuration(ANIMATE_DURATION_SHORT_MILLIS);
+ rejectTextShow.setStartDelay(SWIPE_TO_DECLINE_FADE_IN_DELAY_MILLIS);
+
+ Animator puckUp =
+ ObjectAnimator.ofFloat(
+ contactPuckContainer,
+ View.TRANSLATION_Y,
+ DpUtil.dpToPx(getContext(), 400 /* dp */),
+ DpUtil.dpToPx(getContext(), -12 /* dp */));
+ puckUp.setDuration(ANIMATE_DURATION_LONG_MILLIS);
+ puckUp.setInterpolator(
+ PathInterpolatorCompat.create(
+ 0 /* controlX1 */, 0 /* controlY1 */, 0 /* controlX2 */, 1 /* controlY2 */));
+
+ Animator puckDown =
+ ObjectAnimator.ofFloat(
+ contactPuckContainer,
+ View.TRANSLATION_Y,
+ DpUtil.dpToPx(getContext(), -12 /* dp */),
+ 0 /* end pos */);
+ puckDown.setDuration(ANIMATE_DURATION_NORMAL_MILLIS);
+ puckDown.setInterpolator(new FastOutSlowInInterpolator());
+
+ Animator puckScaleUp =
+ createUniformScaleAnimators(
+ contactPuckBackground,
+ 0.33f /* beginScale */,
+ 1.1f /* endScale */,
+ ANIMATE_DURATION_NORMAL_MILLIS,
+ PathInterpolatorCompat.create(
+ 0.4f /* controlX1 */, 0 /* controlY1 */, 0 /* controlX2 */, 1 /* controlY2 */));
+ Animator puckScaleDown =
+ createUniformScaleAnimators(
+ contactPuckBackground,
+ 1.1f /* beginScale */,
+ 1 /* endScale */,
+ ANIMATE_DURATION_NORMAL_MILLIS,
+ new FastOutSlowInInterpolator());
+
+ // Upward animation chain.
+ lockEntryAnim.play(textUp).with(puckScaleUp).with(puckUp);
+
+ // Downward animation chain.
+ lockEntryAnim.play(textDown).with(puckDown).with(puckScaleDown).after(puckUp);
+
+ lockEntryAnim.play(rejectTextShow).after(puckUp);
+
+ // Add vibration animation.
+ addVibrationAnimator(lockEntryAnim);
+
+ lockEntryAnim.addListener(
+ new AnimatorListenerAdapter() {
+
+ public boolean canceled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ super.onAnimationCancel(animation);
+ canceled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ if (!canceled) {
+ onEntryAnimationDone();
+ }
+ }
+ });
+ lockEntryAnim.start();
+ }
+
+ @VisibleForTesting
+ void onEntryAnimationDone() {
+ LogUtil.i("FlingUpDownMethod.onEntryAnimationDone", "Swipe entry anim ends.");
+ if (animationState == AnimationState.ENTRY) {
+ setAnimationState(AnimationState.BOUNCE);
+ }
+ }
+
+ private void startSwipeToAnswerBounceAnimation() {
+ LogUtil.i("FlingUpDownMethod.startSwipeToAnswerBounceAnimation", "Swipe bounce animation.");
+ endAnimation();
+
+ if (ViewUtil.areAnimationsDisabled(getContext())) {
+ swipeToAnswerText.setTranslationY(0);
+ contactPuckContainer.setTranslationY(0);
+ contactPuckBackground.setScaleY(1f);
+ contactPuckBackground.setScaleX(1f);
+ swipeToRejectText.setAlpha(1f);
+ swipeToRejectText.setTranslationY(0);
+ return;
+ }
+
+ lockBounceAnim = createBreatheAnimation();
+
+ answerHint.onBounceStart();
+ lockBounceAnim.addListener(
+ new AnimatorListenerAdapter() {
+ boolean firstPass = true;
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ if (getContext() != null
+ && lockBounceAnim != null
+ && animationState == AnimationState.BOUNCE) {
+ // AnimatorSet doesn't have repeat settings. Instead, we start a new one after the
+ // previous set is completed, until endAnimation is called.
+ LogUtil.v("FlingUpDownMethod.onAnimationEnd", "Bounce again.");
+
+ // If this is the first time repeating the animation, we should recreate it so its
+ // starting values will be correct
+ if (firstPass) {
+ lockBounceAnim = createBreatheAnimation();
+ lockBounceAnim.addListener(this);
+ }
+ firstPass = false;
+ answerHint.onBounceStart();
+ lockBounceAnim.start();
+ }
+ }
+ });
+ lockBounceAnim.start();
+ }
+
+ private Animator createBreatheAnimation() {
+ AnimatorSet breatheAnimation = new AnimatorSet();
+ float textOffset = DpUtil.dpToPx(getContext(), 42 /* dp */);
+ Animator textUp =
+ ObjectAnimator.ofFloat(
+ swipeToAnswerText, View.TRANSLATION_Y, 0 /* begin pos */, -textOffset);
+ textUp.setInterpolator(new FastOutSlowInInterpolator());
+ textUp.setDuration(ANIMATE_DURATION_NORMAL_MILLIS);
+
+ Animator textDown =
+ ObjectAnimator.ofFloat(swipeToAnswerText, View.TRANSLATION_Y, -textOffset, 0 /* end pos */);
+ textDown.setInterpolator(new FastOutSlowInInterpolator());
+ textDown.setDuration(ANIMATE_DURATION_NORMAL_MILLIS);
+
+ // "Swipe down to reject" text fade in
+ Animator rejectTextShow = ObjectAnimator.ofFloat(swipeToRejectText, View.ALPHA, 1f);
+ rejectTextShow.setInterpolator(new LinearOutSlowInInterpolator());
+ rejectTextShow.setDuration(ANIMATE_DURATION_SHORT_MILLIS);
+ rejectTextShow.setStartDelay(SWIPE_TO_DECLINE_FADE_IN_DELAY_MILLIS);
+
+ // reject hint text translate in
+ Animator rejectTextTranslate =
+ ObjectAnimator.ofFloat(
+ swipeToRejectText,
+ View.TRANSLATION_Y,
+ DpUtil.dpToPx(getContext(), HINT_REJECT_FADE_TRANSLATION_Y_DP),
+ 0f);
+ rejectTextTranslate.setInterpolator(new FastOutSlowInInterpolator());
+ rejectTextTranslate.setDuration(ANIMATE_DURATION_NORMAL_MILLIS);
+
+ // reject hint text fade out
+ Animator rejectTextHide = ObjectAnimator.ofFloat(swipeToRejectText, View.ALPHA, 0f);
+ rejectTextHide.setInterpolator(new FastOutLinearInInterpolator());
+ rejectTextHide.setDuration(ANIMATE_DURATION_SHORT_MILLIS);
+
+ Interpolator curve =
+ PathInterpolatorCompat.create(
+ 0.4f /* controlX1 */, 0 /* controlY1 */, 0 /* controlX2 */, 1 /* controlY2 */);
+ float puckOffset = DpUtil.dpToPx(getContext(), 42 /* dp */);
+ Animator puckUp = ObjectAnimator.ofFloat(contactPuckContainer, View.TRANSLATION_Y, -puckOffset);
+ puckUp.setInterpolator(curve);
+ puckUp.setDuration(ANIMATE_DURATION_LONG_MILLIS);
+
+ final float scale = 1.0625f;
+ Animator puckScaleUp =
+ createUniformScaleAnimators(
+ contactPuckBackground,
+ 1 /* beginScale */,
+ scale,
+ ANIMATE_DURATION_NORMAL_MILLIS,
+ curve);
+
+ Animator puckDown =
+ ObjectAnimator.ofFloat(contactPuckContainer, View.TRANSLATION_Y, 0 /* end pos */);
+ puckDown.setInterpolator(new FastOutSlowInInterpolator());
+ puckDown.setDuration(ANIMATE_DURATION_NORMAL_MILLIS);
+
+ Animator puckScaleDown =
+ createUniformScaleAnimators(
+ contactPuckBackground,
+ scale,
+ 1 /* endScale */,
+ ANIMATE_DURATION_NORMAL_MILLIS,
+ new FastOutSlowInInterpolator());
+
+ // Bounce upward animation chain.
+ breatheAnimation
+ .play(textUp)
+ .with(rejectTextHide)
+ .with(puckUp)
+ .with(puckScaleUp)
+ .after(167 /* delay */);
+
+ // Bounce downward animation chain.
+ breatheAnimation
+ .play(puckDown)
+ .with(textDown)
+ .with(puckScaleDown)
+ .with(rejectTextShow)
+ .with(rejectTextTranslate)
+ .after(puckUp);
+
+ // Add vibration animation to the animator set.
+ addVibrationAnimator(breatheAnimation);
+
+ return breatheAnimation;
+ }
+
+ private void startSwipeToAnswerSettleAnimation() {
+ endAnimation();
+
+ ObjectAnimator puckScale =
+ ObjectAnimator.ofPropertyValuesHolder(
+ contactPuckBackground,
+ PropertyValuesHolder.ofFloat(View.SCALE_X, 1),
+ PropertyValuesHolder.ofFloat(View.SCALE_Y, 1));
+ puckScale.setDuration(SETTLE_ANIMATION_DURATION_MILLIS);
+
+ ObjectAnimator iconRotation = ObjectAnimator.ofFloat(contactPuckIcon, View.ROTATION, 0);
+ iconRotation.setDuration(SETTLE_ANIMATION_DURATION_MILLIS);
+
+ ObjectAnimator swipeToAnswerTextFade =
+ createFadeAnimation(swipeToAnswerText, 1, SETTLE_ANIMATION_DURATION_MILLIS);
+
+ ObjectAnimator contactPuckContainerFade =
+ createFadeAnimation(contactPuckContainer, 1, SETTLE_ANIMATION_DURATION_MILLIS);
+
+ ObjectAnimator contactPuckBackgroundFade =
+ createFadeAnimation(contactPuckBackground, 1, SETTLE_ANIMATION_DURATION_MILLIS);
+
+ ObjectAnimator contactPuckIconFade =
+ createFadeAnimation(
+ contactPuckIcon, shouldShowPhotoInPuck() ? 0 : 1, SETTLE_ANIMATION_DURATION_MILLIS);
+
+ ObjectAnimator contactPuckTranslation =
+ ObjectAnimator.ofPropertyValuesHolder(
+ contactPuckContainer,
+ PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 0),
+ PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 0));
+ contactPuckTranslation.setDuration(SETTLE_ANIMATION_DURATION_MILLIS);
+
+ lockSettleAnim = new AnimatorSet();
+ lockSettleAnim
+ .play(puckScale)
+ .with(iconRotation)
+ .with(swipeToAnswerTextFade)
+ .with(contactPuckContainerFade)
+ .with(contactPuckBackgroundFade)
+ .with(contactPuckIconFade)
+ .with(contactPuckTranslation);
+
+ lockSettleAnim.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ afterSettleAnimationState = AnimationState.NONE;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ onSettleAnimationDone();
+ }
+ });
+
+ lockSettleAnim.start();
+ }
+
+ @VisibleForTesting
+ void onSettleAnimationDone() {
+ if (afterSettleAnimationState != AnimationState.NONE) {
+ int nextState = afterSettleAnimationState;
+ afterSettleAnimationState = AnimationState.NONE;
+ lockSettleAnim = null;
+
+ setAnimationState(nextState);
+ }
+ }
+
+ private ObjectAnimator createFadeAnimation(View target, float targetAlpha, long duration) {
+ ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(target, View.ALPHA, targetAlpha);
+ objectAnimator.setDuration(duration);
+ return objectAnimator;
+ }
+
+ private void startSwipeToAnswerHintAnimation() {
+ if (rejectHintHide != null) {
+ rejectHintHide.cancel();
+ }
+
+ endAnimation();
+ resetTouchState();
+
+ if (ViewUtil.areAnimationsDisabled(getContext())) {
+ onHintAnimationDone(false);
+ return;
+ }
+
+ lockHintAnim = new AnimatorSet();
+ float jumpOffset = DpUtil.dpToPx(getContext(), HINT_JUMP_DP);
+ float dipOffset = DpUtil.dpToPx(getContext(), HINT_DIP_DP);
+ float scaleSize = HINT_SCALE_RATIO;
+ float textOffset = jumpOffset + (scaleSize - 1) * contactPuckBackground.getHeight();
+ int shortAnimTime =
+ getContext().getResources().getInteger(android.R.integer.config_shortAnimTime);
+ int mediumAnimTime =
+ getContext().getResources().getInteger(android.R.integer.config_mediumAnimTime);
+
+ // Puck squashes to anticipate jump
+ ObjectAnimator puckAnticipate =
+ ObjectAnimator.ofPropertyValuesHolder(
+ contactPuckContainer,
+ PropertyValuesHolder.ofFloat(View.SCALE_Y, .95f),
+ PropertyValuesHolder.ofFloat(View.SCALE_X, 1.05f));
+ puckAnticipate.setRepeatCount(1);
+ puckAnticipate.setRepeatMode(ValueAnimator.REVERSE);
+ puckAnticipate.setDuration(shortAnimTime / 2);
+ puckAnticipate.setInterpolator(new DecelerateInterpolator());
+ puckAnticipate.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ contactPuckContainer.setPivotY(contactPuckContainer.getHeight());
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ contactPuckContainer.setPivotY(contactPuckContainer.getHeight() / 2);
+ }
+ });
+
+ // Ensure puck is at the right starting point for the jump
+ ObjectAnimator puckResetTranslation =
+ ObjectAnimator.ofPropertyValuesHolder(
+ contactPuckContainer,
+ PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 0),
+ PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 0));
+ puckResetTranslation.setDuration(shortAnimTime / 2);
+ puckAnticipate.setInterpolator(new DecelerateInterpolator());
+
+ Animator textUp = ObjectAnimator.ofFloat(swipeToAnswerText, View.TRANSLATION_Y, -textOffset);
+ textUp.setInterpolator(new LinearOutSlowInInterpolator());
+ textUp.setDuration(shortAnimTime);
+
+ Animator puckUp = ObjectAnimator.ofFloat(contactPuckContainer, View.TRANSLATION_Y, -jumpOffset);
+ puckUp.setInterpolator(new LinearOutSlowInInterpolator());
+ puckUp.setDuration(shortAnimTime);
+
+ Animator puckScaleUp =
+ createUniformScaleAnimators(
+ contactPuckBackground, 1f, scaleSize, shortAnimTime, new LinearOutSlowInInterpolator());
+
+ Animator rejectHintShow =
+ ObjectAnimator.ofPropertyValuesHolder(
+ swipeToRejectText,
+ PropertyValuesHolder.ofFloat(View.ALPHA, 1f),
+ PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 0f));
+ rejectHintShow.setDuration(shortAnimTime);
+
+ Animator rejectHintDip =
+ ObjectAnimator.ofFloat(swipeToRejectText, View.TRANSLATION_Y, dipOffset);
+ rejectHintDip.setInterpolator(new LinearOutSlowInInterpolator());
+ rejectHintDip.setDuration(shortAnimTime);
+
+ Animator textDown = ObjectAnimator.ofFloat(swipeToAnswerText, View.TRANSLATION_Y, 0);
+ textDown.setInterpolator(new LinearOutSlowInInterpolator());
+ textDown.setDuration(mediumAnimTime);
+
+ Animator puckDown = ObjectAnimator.ofFloat(contactPuckContainer, View.TRANSLATION_Y, 0);
+ BounceInterpolator bounce = new BounceInterpolator();
+ puckDown.setInterpolator(bounce);
+ puckDown.setDuration(mediumAnimTime);
+
+ Animator puckScaleDown =
+ createUniformScaleAnimators(
+ contactPuckBackground, scaleSize, 1f, shortAnimTime, new LinearOutSlowInInterpolator());
+
+ Animator rejectHintUp = ObjectAnimator.ofFloat(swipeToRejectText, View.TRANSLATION_Y, 0);
+ rejectHintUp.setInterpolator(new LinearOutSlowInInterpolator());
+ rejectHintUp.setDuration(mediumAnimTime);
+
+ lockHintAnim.play(puckAnticipate).with(puckResetTranslation).before(puckUp);
+ lockHintAnim
+ .play(textUp)
+ .with(puckUp)
+ .with(puckScaleUp)
+ .with(rejectHintDip)
+ .with(rejectHintShow);
+ lockHintAnim.play(textDown).with(puckDown).with(puckScaleDown).with(rejectHintUp).after(puckUp);
+ lockHintAnim.start();
+
+ rejectHintHide = ObjectAnimator.ofFloat(swipeToRejectText, View.ALPHA, 0);
+ rejectHintHide.setStartDelay(HINT_REJECT_SHOW_DURATION_MILLIS);
+ rejectHintHide.addListener(
+ new AnimatorListenerAdapter() {
+
+ private boolean canceled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ super.onAnimationCancel(animation);
+ canceled = true;
+ rejectHintHide = null;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ onHintAnimationDone(canceled);
+ }
+ });
+ rejectHintHide.start();
+ }
+
+ @VisibleForTesting
+ void onHintAnimationDone(boolean canceled) {
+ if (!canceled && animationState == AnimationState.HINT) {
+ setAnimationState(AnimationState.BOUNCE);
+ }
+ rejectHintHide = null;
+ }
+
+ private void clearSwipeToAnswerUi() {
+ LogUtil.i("FlingUpDownMethod.clearSwipeToAnswerUi", "Clear swipe animation.");
+ endAnimation();
+ swipeToAnswerText.setVisibility(View.GONE);
+ contactPuckContainer.setVisibility(View.GONE);
+ }
+
+ private void endAnimation() {
+ LogUtil.i("FlingUpDownMethod.endAnimation", "End animations.");
+ if (lockSettleAnim != null) {
+ lockSettleAnim.cancel();
+ lockSettleAnim = null;
+ }
+ if (lockBounceAnim != null) {
+ lockBounceAnim.cancel();
+ lockBounceAnim = null;
+ }
+ if (lockEntryAnim != null) {
+ lockEntryAnim.cancel();
+ lockEntryAnim = null;
+ }
+ if (lockHintAnim != null) {
+ lockHintAnim.cancel();
+ lockHintAnim = null;
+ }
+ if (rejectHintHide != null) {
+ rejectHintHide.cancel();
+ rejectHintHide = null;
+ }
+ if (vibrationAnimator != null) {
+ vibrationAnimator.end();
+ vibrationAnimator = null;
+ }
+ answerHint.onBounceEnd();
+ }
+
+ // Create an animator to scale on X/Y directions uniformly.
+ private Animator createUniformScaleAnimators(
+ View target, float begin, float end, long duration, Interpolator interpolator) {
+ ObjectAnimator animator =
+ ObjectAnimator.ofPropertyValuesHolder(
+ target,
+ PropertyValuesHolder.ofFloat(View.SCALE_X, begin, end),
+ PropertyValuesHolder.ofFloat(View.SCALE_Y, begin, end));
+ animator.setDuration(duration);
+ animator.setInterpolator(interpolator);
+ return animator;
+ }
+
+ private void addVibrationAnimator(AnimatorSet animatorSet) {
+ if (vibrationAnimator != null) {
+ vibrationAnimator.end();
+ }
+
+ // Note that we animate the value between 0 and 1, but internally VibrateInterpolator will
+ // translate it into actually X translation value.
+ vibrationAnimator =
+ ObjectAnimator.ofFloat(
+ contactPuckContainer, View.TRANSLATION_X, 0 /* begin value */, 1 /* end value */);
+ vibrationAnimator.setDuration(VIBRATION_TIME_MILLIS);
+ vibrationAnimator.setInterpolator(new VibrateInterpolator(getContext()));
+
+ animatorSet.play(vibrationAnimator).after(0 /* delay */);
+ }
+
+ private void performAccept() {
+ LogUtil.i("FlingUpDownMethod.performAccept", null);
+ swipeToAnswerText.setVisibility(View.GONE);
+ contactPuckContainer.setVisibility(View.GONE);
+
+ // Complete the animation loop.
+ setAnimationState(AnimationState.COMPLETED);
+ getParent().answerFromMethod();
+ }
+
+ private void performReject() {
+ LogUtil.i("FlingUpDownMethod.performReject", null);
+ swipeToAnswerText.setVisibility(View.GONE);
+ contactPuckContainer.setVisibility(View.GONE);
+
+ // Complete the animation loop.
+ setAnimationState(AnimationState.COMPLETED);
+ getParent().rejectFromMethod();
+ }
+
+ /** Custom interpolator class for puck vibration. */
+ private static class VibrateInterpolator implements Interpolator {
+
+ private static final long RAMP_UP_BEGIN_MS = 583;
+ private static final long RAMP_UP_DURATION_MS = 167;
+ private static final long RAMP_UP_END_MS = RAMP_UP_BEGIN_MS + RAMP_UP_DURATION_MS;
+ private static final long RAMP_DOWN_BEGIN_MS = 1_583;
+ private static final long RAMP_DOWN_DURATION_MS = 250;
+ private static final long RAMP_DOWN_END_MS = RAMP_DOWN_BEGIN_MS + RAMP_DOWN_DURATION_MS;
+ private static final long RAMP_TOTAL_TIME_MS = RAMP_DOWN_END_MS;
+ private final float ampMax;
+ private final float freqMax = 80;
+ private Interpolator sliderInterpolator = new FastOutSlowInInterpolator();
+
+ VibrateInterpolator(Context context) {
+ ampMax = DpUtil.dpToPx(context, 1 /* dp */);
+ }
+
+ @Override
+ public float getInterpolation(float t) {
+ float slider = 0;
+ float time = t * RAMP_TOTAL_TIME_MS;
+
+ // Calculate the slider value based on RAMP_UP and RAMP_DOWN times. Between RAMP_UP and
+ // RAMP_DOWN, the slider remains the maximum value of 1.
+ if (time > RAMP_UP_BEGIN_MS && time < RAMP_UP_END_MS) {
+ // Ramp up.
+ slider =
+ sliderInterpolator.getInterpolation(
+ (time - RAMP_UP_BEGIN_MS) / (float) RAMP_UP_DURATION_MS);
+ } else if ((time >= RAMP_UP_END_MS) && time <= RAMP_DOWN_BEGIN_MS) {
+ // Vibrate at maximum
+ slider = 1;
+ } else if (time > RAMP_DOWN_BEGIN_MS && time < RAMP_DOWN_END_MS) {
+ // Ramp down.
+ slider =
+ 1
+ - sliderInterpolator.getInterpolation(
+ (time - RAMP_DOWN_BEGIN_MS) / (float) RAMP_DOWN_DURATION_MS);
+ }
+
+ float ampNormalized = ampMax * slider;
+ float freqNormalized = freqMax * slider;
+
+ return (float) (ampNormalized * Math.sin(time * freqNormalized));
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java
new file mode 100644
index 000000000..a21073d65
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java
@@ -0,0 +1,496 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.answermethod;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.support.annotation.FloatRange;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
+import com.android.dialer.common.DpUtil;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.MathUtil;
+import com.android.incallui.answer.impl.classifier.FalsingManager;
+import com.android.incallui.answer.impl.utils.FlingAnimationUtils;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Touch handler that keeps track of flings for {@link FlingUpDownMethod}. */
+@SuppressLint("ClickableViewAccessibility")
+class FlingUpDownTouchHandler implements OnTouchListener {
+
+ /** Callback interface for significant events with this touch handler */
+ interface OnProgressChangedListener {
+
+ /**
+ * Called when the visible answer progress has changed. Implementations should use this for
+ * animation, but should not perform accepts or rejects until {@link #onMoveFinish(boolean)} is
+ * called.
+ *
+ * @param progress float representation of the progress with +1f fully accepted, -1f fully
+ * rejected, and 0 neutral.
+ */
+ void onProgressChanged(@FloatRange(from = -1f, to = 1f) float progress);
+
+ /** Called when a touch event has started being tracked. */
+ void onTrackingStart();
+
+ /** Called when touch events stop being tracked. */
+ void onTrackingStopped();
+
+ /**
+ * Called when the progress has fully animated back to neutral. Normal resting animation should
+ * resume, possibly with a hint animation first.
+ *
+ * @param showHint {@code true} iff the hint animation should be run before resuming normal
+ * animation.
+ */
+ void onMoveReset(boolean showHint);
+
+ /**
+ * Called when the progress has animated fully to accept or reject.
+ *
+ * @param accept {@code true} if the call has been accepted, {@code false} if it has been
+ * rejected.
+ */
+ void onMoveFinish(boolean accept);
+
+ /**
+ * Determine whether this gesture should use the {@link FalsingManager} to reject accidental
+ * touches
+ *
+ * @param downEvent the MotionEvent corresponding to the start of the gesture
+ * @return {@code true} if the {@link FalsingManager} should be used to reject accidental
+ * touches for this gesture
+ */
+ boolean shouldUseFalsing(@NonNull MotionEvent downEvent);
+ }
+
+ // Progress that must be moved through to not show the hint animation after gesture completes
+ private static final float HINT_MOVE_THRESHOLD_RATIO = .1f;
+ // Dp touch needs to move upward to be considered fully accepted
+ private static final int ACCEPT_THRESHOLD_DP = 150;
+ // Dp touch needs to move downward to be considered fully rejected
+ private static final int REJECT_THRESHOLD_DP = 150;
+ // Dp touch needs to move for it to not be considered a false touch (if FalsingManager is not
+ // enabled)
+ private static final int FALSING_THRESHOLD_DP = 40;
+
+ // Progress at which a fling in the opposite direction will recenter instead of
+ // accepting/rejecting
+ private static final float PROGRESS_FLING_RECENTER = .1f;
+
+ // Progress at which a slow swipe would continue toward accept/reject after the
+ // touch has been let go, otherwise will recenter
+ private static final float PROGRESS_SWIPE_RECENTER = .8f;
+
+ private static final float REJECT_FLING_THRESHOLD_MODIFIER = 2f;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({FlingTarget.CENTER, FlingTarget.ACCEPT, FlingTarget.REJECT})
+ private @interface FlingTarget {
+ int CENTER = 0;
+ int ACCEPT = 1;
+ int REJECT = -1;
+ }
+
+ /**
+ * Create a new FlingUpDownTouchHandler and attach it to the target. Will call {@link
+ * View#setOnTouchListener(OnTouchListener)} before returning.
+ *
+ * @param target View whose touches are to be listened to
+ * @param listener Callback to listen to major events
+ * @param falsingManager FalsingManager to identify false touches
+ * @return the instance of FlingUpDownTouchHandler that has been added as a touch listener
+ */
+ public static FlingUpDownTouchHandler attach(
+ @NonNull View target,
+ @NonNull OnProgressChangedListener listener,
+ @Nullable FalsingManager falsingManager) {
+ FlingUpDownTouchHandler handler = new FlingUpDownTouchHandler(target, listener, falsingManager);
+ target.setOnTouchListener(handler);
+ return handler;
+ }
+
+ @NonNull private final View target;
+ @NonNull private final OnProgressChangedListener listener;
+
+ private VelocityTracker velocityTracker;
+ private FlingAnimationUtils flingAnimationUtils;
+
+ private boolean touchEnabled = true;
+ private boolean flingEnabled = true;
+ private float currentProgress;
+ private boolean tracking;
+
+ private boolean motionAborted;
+ private boolean touchSlopExceeded;
+ private boolean hintDistanceExceeded;
+ private int trackingPointer;
+ private Animator progressAnimator;
+
+ private float touchSlop;
+ private float initialTouchY;
+ private float acceptThresholdY;
+ private float rejectThresholdY;
+ private float zeroY;
+
+ private boolean touchAboveFalsingThreshold;
+ private float falsingThresholdPx;
+ private boolean touchUsesFalsing;
+
+ private final float acceptThresholdPx;
+ private final float rejectThresholdPx;
+ private final float deadZoneTopPx;
+
+ @Nullable private final FalsingManager falsingManager;
+
+ private FlingUpDownTouchHandler(
+ @NonNull View target,
+ @NonNull OnProgressChangedListener listener,
+ @Nullable FalsingManager falsingManager) {
+ this.target = target;
+ this.listener = listener;
+ Context context = target.getContext();
+ touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ flingAnimationUtils = new FlingAnimationUtils(context, .6f);
+ falsingThresholdPx = DpUtil.dpToPx(context, FALSING_THRESHOLD_DP);
+ acceptThresholdPx = DpUtil.dpToPx(context, ACCEPT_THRESHOLD_DP);
+ rejectThresholdPx = DpUtil.dpToPx(context, REJECT_THRESHOLD_DP);
+
+ deadZoneTopPx =
+ Math.max(
+ context.getResources().getDimension(R.dimen.answer_swipe_dead_zone_top),
+ acceptThresholdPx);
+ this.falsingManager = falsingManager;
+ }
+
+ /** Returns {@code true} iff a touch is being tracked */
+ public boolean isTracking() {
+ return tracking;
+ }
+
+ /**
+ * Sets whether touch events will continue to be listened to
+ *
+ * @param touchEnabled whether future touch events will be listened to
+ */
+ public void setTouchEnabled(boolean touchEnabled) {
+ this.touchEnabled = touchEnabled;
+ }
+
+ /**
+ * Sets whether fling velocity is used to affect accept/reject behavior
+ *
+ * @param flingEnabled whether fling velocity will be used when determining whether to
+ * accept/reject or recenter
+ */
+ public void setFlingEnabled(boolean flingEnabled) {
+ this.flingEnabled = flingEnabled;
+ }
+
+ public void detach() {
+ cancelProgressAnimator();
+ setTouchEnabled(false);
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (falsingManager != null) {
+ falsingManager.onTouchEvent(event);
+ }
+ if (!touchEnabled) {
+ return false;
+ }
+ if (motionAborted && (event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
+ return false;
+ }
+
+ int pointerIndex = event.findPointerIndex(trackingPointer);
+ if (pointerIndex < 0) {
+ pointerIndex = 0;
+ trackingPointer = event.getPointerId(pointerIndex);
+ }
+ final float pointerY = event.getY(pointerIndex);
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ if (pointerY < deadZoneTopPx) {
+ return false;
+ }
+ motionAborted = false;
+ startMotion(pointerY, false, currentProgress);
+ touchAboveFalsingThreshold = false;
+ touchUsesFalsing = listener.shouldUseFalsing(event);
+ if (velocityTracker == null) {
+ initVelocityTracker();
+ }
+ trackMovement(event);
+ cancelProgressAnimator();
+ touchSlopExceeded = progressAnimator != null;
+ onTrackingStarted();
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ final int upPointer = event.getPointerId(event.getActionIndex());
+ if (trackingPointer == upPointer) {
+ // gesture is ongoing, find a new pointer to track
+ int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+ float newY = event.getY(newIndex);
+ trackingPointer = event.getPointerId(newIndex);
+ startMotion(newY, true, currentProgress);
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ motionAborted = true;
+ endMotionEvent(event, pointerY, true);
+ return false;
+ case MotionEvent.ACTION_MOVE:
+ float deltaY = pointerY - initialTouchY;
+
+ if (Math.abs(deltaY) > touchSlop) {
+ touchSlopExceeded = true;
+ }
+ if (Math.abs(deltaY) >= falsingThresholdPx) {
+ touchAboveFalsingThreshold = true;
+ }
+ setCurrentProgress(pointerYToProgress(pointerY));
+ trackMovement(event);
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ trackMovement(event);
+ endMotionEvent(event, pointerY, false);
+ }
+ return true;
+ }
+
+ private void endMotionEvent(MotionEvent event, float pointerY, boolean forceCancel) {
+ trackingPointer = -1;
+ if ((tracking && touchSlopExceeded)
+ || Math.abs(pointerY - initialTouchY) > touchSlop
+ || event.getActionMasked() == MotionEvent.ACTION_CANCEL
+ || forceCancel) {
+ float vel = 0f;
+ float vectorVel = 0f;
+ if (velocityTracker != null) {
+ velocityTracker.computeCurrentVelocity(1000);
+ vel = velocityTracker.getYVelocity();
+ vectorVel =
+ Math.copySign(
+ (float) Math.hypot(velocityTracker.getXVelocity(), velocityTracker.getYVelocity()),
+ vel);
+ }
+
+ boolean falseTouch = isFalseTouch();
+ boolean forceRecenter =
+ falseTouch
+ || !touchSlopExceeded
+ || forceCancel
+ || event.getActionMasked() == MotionEvent.ACTION_CANCEL;
+
+ @FlingTarget
+ int target = forceRecenter ? FlingTarget.CENTER : getFlingTarget(pointerY, vectorVel);
+
+ fling(vel, target, falseTouch);
+ onTrackingStopped();
+ } else {
+ onTrackingStopped();
+ setCurrentProgress(0);
+ onMoveEnded();
+ }
+
+ if (velocityTracker != null) {
+ velocityTracker.recycle();
+ velocityTracker = null;
+ }
+ }
+
+ @FlingTarget
+ private int getFlingTarget(float pointerY, float vectorVel) {
+ float progress = pointerYToProgress(pointerY);
+
+ float minVelocityPxPerSecond = flingAnimationUtils.getMinVelocityPxPerSecond();
+ if (vectorVel > 0) {
+ minVelocityPxPerSecond *= REJECT_FLING_THRESHOLD_MODIFIER;
+ }
+ if (!flingEnabled || Math.abs(vectorVel) < minVelocityPxPerSecond) {
+ // Not a fling
+ if (Math.abs(progress) > PROGRESS_SWIPE_RECENTER) {
+ // Progress near one of the edges
+ return progress > 0 ? FlingTarget.ACCEPT : FlingTarget.REJECT;
+ } else {
+ return FlingTarget.CENTER;
+ }
+ }
+
+ boolean sameDirection = vectorVel < 0 == progress > 0;
+ if (!sameDirection && Math.abs(progress) >= PROGRESS_FLING_RECENTER) {
+ // Being flung back toward center
+ return FlingTarget.CENTER;
+ }
+ // Flung toward an edge
+ return vectorVel < 0 ? FlingTarget.ACCEPT : FlingTarget.REJECT;
+ }
+
+ @FloatRange(from = -1f, to = 1f)
+ private float pointerYToProgress(float pointerY) {
+ boolean pointerAboveZero = pointerY > zeroY;
+ float nearestThreshold = pointerAboveZero ? rejectThresholdY : acceptThresholdY;
+
+ float absoluteProgress = (pointerY - zeroY) / (nearestThreshold - zeroY);
+ return MathUtil.clamp(absoluteProgress * (pointerAboveZero ? -1 : 1), -1f, 1f);
+ }
+
+ private boolean isFalseTouch() {
+ if (falsingManager != null && falsingManager.isEnabled()) {
+ if (falsingManager.isFalseTouch()) {
+ if (touchUsesFalsing) {
+ LogUtil.i("FlingUpDownTouchHandler.isFalseTouch", "rejecting false touch");
+ return true;
+ } else {
+ LogUtil.i(
+ "FlingUpDownTouchHandler.isFalseTouch",
+ "Suspected false touch, but not using false touch rejection for this gesture");
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+ return !touchAboveFalsingThreshold;
+ }
+
+ private void trackMovement(MotionEvent event) {
+ if (velocityTracker != null) {
+ velocityTracker.addMovement(event);
+ }
+ }
+
+ private void fling(float velocity, @FlingTarget int target, boolean centerBecauseOfFalsing) {
+ ValueAnimator animator = createProgressAnimator(target);
+ if (target == FlingTarget.CENTER) {
+ flingAnimationUtils.apply(animator, currentProgress, target, velocity);
+ } else {
+ flingAnimationUtils.applyDismissing(animator, currentProgress, target, velocity, 1);
+ }
+ if (target == FlingTarget.CENTER && centerBecauseOfFalsing) {
+ velocity = 0;
+ }
+ if (velocity == 0) {
+ animator.setDuration(350);
+ }
+
+ animator.addListener(
+ new AnimatorListenerAdapter() {
+ boolean canceled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ canceled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ progressAnimator = null;
+ if (!canceled) {
+ onMoveEnded();
+ }
+ }
+ });
+ progressAnimator = animator;
+ animator.start();
+ }
+
+ private void onMoveEnded() {
+ if (currentProgress == 0) {
+ listener.onMoveReset(!hintDistanceExceeded);
+ } else {
+ listener.onMoveFinish(currentProgress > 0);
+ }
+ }
+
+ private ValueAnimator createProgressAnimator(float targetProgress) {
+ ValueAnimator animator = ValueAnimator.ofFloat(currentProgress, targetProgress);
+ animator.addUpdateListener(
+ new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ setCurrentProgress((Float) animation.getAnimatedValue());
+ }
+ });
+ return animator;
+ }
+
+ private void initVelocityTracker() {
+ if (velocityTracker != null) {
+ velocityTracker.recycle();
+ }
+ velocityTracker = VelocityTracker.obtain();
+ }
+
+ private void startMotion(float newY, boolean startTracking, float startProgress) {
+ initialTouchY = newY;
+ hintDistanceExceeded = false;
+
+ if (startProgress <= .25) {
+ acceptThresholdY = Math.max(0, initialTouchY - acceptThresholdPx);
+ rejectThresholdY = Math.min(target.getHeight(), initialTouchY + rejectThresholdPx);
+ zeroY = initialTouchY;
+ }
+
+ if (startTracking) {
+ touchSlopExceeded = true;
+ onTrackingStarted();
+ setCurrentProgress(startProgress);
+ }
+ }
+
+ private void onTrackingStarted() {
+ tracking = true;
+ listener.onTrackingStart();
+ }
+
+ private void onTrackingStopped() {
+ tracking = false;
+ listener.onTrackingStopped();
+ }
+
+ private void cancelProgressAnimator() {
+ if (progressAnimator != null) {
+ progressAnimator.cancel();
+ }
+ }
+
+ private void setCurrentProgress(float progress) {
+ if (Math.abs(progress) > HINT_MOVE_THRESHOLD_RATIO) {
+ hintDistanceExceeded = true;
+ }
+ currentProgress = progress;
+ listener.onProgressChanged(progress);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/answermethod/TwoButtonMethod.java b/java/com/android/incallui/answer/impl/answermethod/TwoButtonMethod.java
new file mode 100644
index 000000000..67b1b9689
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/TwoButtonMethod.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.answermethod;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.os.Bundle;
+import android.support.annotation.FloatRange;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.ActivityCompat;
+import com.android.incallui.answer.impl.answermethod.FlingUpDownTouchHandler.OnProgressChangedListener;
+import com.android.incallui.util.AccessibilityUtil;
+
+/** Answer method that shows two buttons for answer/reject. */
+public class TwoButtonMethod extends AnswerMethod
+ implements OnClickListener, AnimatorUpdateListener {
+
+ private static final String STATE_HINT_TEXT = "hintText";
+ private static final String STATE_INCOMING_WILL_DISCONNECT = "incomingWillDisconnect";
+
+ private View answerButton;
+ private View answerLabel;
+ private View declineButton;
+ private View declineLabel;
+ private TextView hintTextView;
+ private boolean incomingWillDisconnect;
+ private boolean buttonClicked;
+ private CharSequence hintText;
+ @Nullable private FlingUpDownTouchHandler touchHandler;
+
+ @Override
+ public void onCreate(@Nullable Bundle bundle) {
+ super.onCreate(bundle);
+ if (bundle != null) {
+ incomingWillDisconnect = bundle.getBoolean(STATE_INCOMING_WILL_DISCONNECT);
+ hintText = bundle.getCharSequence(STATE_HINT_TEXT);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle bundle) {
+ super.onSaveInstanceState(bundle);
+ bundle.putBoolean(STATE_INCOMING_WILL_DISCONNECT, incomingWillDisconnect);
+ bundle.putCharSequence(STATE_HINT_TEXT, hintText);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+ View view = layoutInflater.inflate(R.layout.two_button_method, viewGroup, false);
+
+ hintTextView = (TextView) view.findViewById(R.id.two_button_hint_text);
+ updateHintText();
+
+ answerButton = view.findViewById(R.id.two_button_answer_button);
+ answerLabel = view.findViewById(R.id.two_button_answer_label);
+ declineButton = view.findViewById(R.id.two_button_decline_button);
+ declineLabel = view.findViewById(R.id.two_button_decline_label);
+
+ boolean showLabels = getResources().getBoolean(R.bool.two_button_show_button_labels);
+ answerLabel.setVisibility(showLabels ? View.VISIBLE : View.GONE);
+ declineLabel.setVisibility(showLabels ? View.VISIBLE : View.GONE);
+
+ answerButton.setOnClickListener(this);
+ declineButton.setOnClickListener(this);
+
+ if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) {
+ /* Falsing already handled by AccessibilityManager */
+ touchHandler =
+ FlingUpDownTouchHandler.attach(
+ view,
+ new OnProgressChangedListener() {
+ @Override
+ public void onProgressChanged(@FloatRange(from = -1f, to = 1f) float progress) {}
+
+ @Override
+ public void onTrackingStart() {}
+
+ @Override
+ public void onTrackingStopped() {}
+
+ @Override
+ public void onMoveReset(boolean showHint) {}
+
+ @Override
+ public void onMoveFinish(boolean accept) {
+ if (accept) {
+ answerCall();
+ } else {
+ rejectCall();
+ }
+ }
+
+ @Override
+ public boolean shouldUseFalsing(@NonNull MotionEvent downEvent) {
+ return false;
+ }
+ },
+ null /* Falsing already handled by AccessibilityManager */);
+ touchHandler.setFlingEnabled(false);
+ }
+ return view;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ if (touchHandler != null) {
+ touchHandler.detach();
+ touchHandler = null;
+ }
+ }
+
+ @Override
+ public void setHintText(@Nullable CharSequence hintText) {
+ this.hintText = hintText;
+ updateHintText();
+ }
+
+ @Override
+ public void setShowIncomingWillDisconnect(boolean incomingWillDisconnect) {
+ this.incomingWillDisconnect = incomingWillDisconnect;
+ updateHintText();
+ }
+
+ private void updateHintText() {
+ if (hintTextView == null) {
+ return;
+ }
+ hintTextView.setVisibility(
+ ActivityCompat.isInMultiWindowMode(getActivity()) ? View.GONE : View.VISIBLE);
+ if (!TextUtils.isEmpty(hintText) && !buttonClicked) {
+ hintTextView.setText(hintText);
+ hintTextView.animate().alpha(1f).start();
+ } else if (incomingWillDisconnect && !buttonClicked) {
+ hintTextView.setText(R.string.call_incoming_will_disconnect);
+ hintTextView.animate().alpha(1f).start();
+ } else {
+ hintTextView.animate().alpha(0f).start();
+ }
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view == answerButton) {
+ answerCall();
+ LogUtil.v("TwoButtonMethod.onClick", "Call answered");
+ } else if (view == declineButton) {
+ rejectCall();
+ LogUtil.v("TwoButtonMethod.onClick", "two_buttonMethod Call rejected");
+ } else {
+ Assert.fail("Unknown click from view: " + view);
+ }
+ buttonClicked = true;
+ }
+
+ private void answerCall() {
+ ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
+ animator.addUpdateListener(this);
+ animator.addListener(
+ new AnimatorListenerAdapter() {
+ private boolean canceled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ canceled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!canceled) {
+ getParent().answerFromMethod();
+ }
+ }
+ });
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.play(animator).with(createViewHideAnimation());
+ animatorSet.start();
+ }
+
+ private void rejectCall() {
+ ValueAnimator animator = ValueAnimator.ofFloat(0, -1);
+ animator.addUpdateListener(this);
+ animator.addListener(
+ new AnimatorListenerAdapter() {
+ private boolean canceled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ canceled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!canceled) {
+ getParent().rejectFromMethod();
+ }
+ }
+ });
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.play(animator).with(createViewHideAnimation());
+ animatorSet.start();
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ getParent().onAnswerProgressUpdate(((float) animation.getAnimatedValue()));
+ }
+
+ private Animator createViewHideAnimation() {
+ ObjectAnimator answerButtonHide =
+ ObjectAnimator.ofPropertyValuesHolder(
+ answerButton,
+ PropertyValuesHolder.ofFloat(View.SCALE_X, 0f),
+ PropertyValuesHolder.ofFloat(View.SCALE_Y, 0f));
+
+ ObjectAnimator declineButtonHide =
+ ObjectAnimator.ofPropertyValuesHolder(
+ declineButton,
+ PropertyValuesHolder.ofFloat(View.SCALE_X, 0f),
+ PropertyValuesHolder.ofFloat(View.SCALE_Y, 0f));
+
+ ObjectAnimator answerLabelHide = ObjectAnimator.ofFloat(answerLabel, View.ALPHA, 0f);
+
+ ObjectAnimator declineLabelHide = ObjectAnimator.ofFloat(declineLabel, View.ALPHA, 0f);
+
+ ObjectAnimator hintHide = ObjectAnimator.ofFloat(hintTextView, View.ALPHA, 0f);
+
+ AnimatorSet hideSet = new AnimatorSet();
+ hideSet
+ .play(answerButtonHide)
+ .with(declineButtonHide)
+ .with(answerLabelHide)
+ .with(declineLabelHide)
+ .with(hintHide);
+ return hideSet;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/drawable/call_answer.xml b/java/com/android/incallui/answer/impl/answermethod/res/drawable/call_answer.xml
new file mode 100644
index 000000000..451c862fa
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/drawable/call_answer.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="24dp"
+ android:viewportHeight="32.0"
+ android:viewportWidth="32.0"
+ android:width="24dp">
+ <group
+ android:name="rotationGroup"
+ android:pivotX="12"
+ android:pivotY="12"
+ android:translateX="4"
+ android:translateY="4"
+ android:rotation="0"
+ >
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M6.62,10.79c1.44,2.83 3.76,5.14 6.59,6.59l2.2,-2.2c0.27,-0.27 0.67,-0.36 1.02,-0.24 1.12,0.37 2.33,0.57 3.57,0.57 0.55,0 1,0.45 1,1V20c0,0.55 -0.45,1 -1,1 -9.39,0 -17,-7.61 -17,-17 0,-0.55 0.45,-1 1,-1h3.5c0.55,0 1,0.45 1,1 0,1.25 0.2,2.45 0.57,3.57 0.11,0.35 0.03,0.74 -0.25,1.02l-2.2,2.2z"/>
+ </group>
+</vector>
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/drawable/circular_background.xml b/java/com/android/incallui/answer/impl/answermethod/res/drawable/circular_background.xml
new file mode 100644
index 000000000..938ddc2be
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/drawable/circular_background.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+ <solid android:color="#FFFFFFFF"/>
+</shape>
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/layout/swipe_up_down_method.xml b/java/com/android/incallui/answer/impl/answermethod/res/layout/swipe_up_down_method.xml
new file mode 100644
index 000000000..78e097958
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/layout/swipe_up_down_method.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginStart="@dimen/answer_swipe_dead_zone_sides"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:layout_marginEnd="@dimen/answer_swipe_dead_zone_sides">
+ <LinearLayout
+ android:id="@+id/incoming_swipe_to_answer_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:accessibilityLiveRegion="polite"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:gravity="center_horizontal|bottom"
+ android:orientation="vertical"
+ android:visibility="visible">
+ <TextView
+ android:id="@+id/incoming_will_disconnect_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="116dp"
+ android:layout_gravity="center_horizontal"
+ android:alpha="0"
+ android:text="@string/call_incoming_will_disconnect"
+ android:textColor="@color/blue_grey_100"
+ android:textSize="16sp"
+ tools:alpha="1"/>
+ <TextView
+ android:id="@+id/incoming_swipe_to_answer_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="18dp"
+ android:layout_gravity="center_horizontal"
+ android:focusable="false"
+ android:text="@string/call_incoming_swipe_to_answer"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance.Hint"/>
+
+ <FrameLayout
+ android:id="@+id/incoming_call_puck_container"
+ android:layout_width="@dimen/answer_contact_puck_size_photo"
+ android:layout_height="@dimen/answer_contact_puck_size_photo"
+ android:layout_marginBottom="10dp"
+ android:layout_gravity="center_horizontal"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:contentDescription="@string/a11y_incoming_call_swipe_to_answer">
+
+ <!-- Puck background and icon are hosted in the separated views to animate separately. -->
+ <ImageView
+ android:id="@+id/incoming_call_puck_bg"
+ android:layout_width="@dimen/answer_contact_puck_size_no_photo"
+ android:layout_height="@dimen/answer_contact_puck_size_no_photo"
+ android:layout_gravity="center"
+ android:background="@drawable/circular_background"
+ android:contentDescription="@null"
+ android:duplicateParentState="true"
+ android:elevation="8dp"
+ android:focusable="false"
+ android:stateListAnimator="@animator/activated_button_elevation"/>
+
+ <ImageView
+ android:id="@+id/incoming_call_puck_icon"
+ android:layout_width="30dp"
+ android:layout_height="30dp"
+ android:layout_gravity="center"
+ android:contentDescription="@null"
+ android:duplicateParentState="true"
+ android:elevation="16dp"
+ android:focusable="false"
+ android:outlineProvider="none"
+ android:src="@drawable/quantum_ic_call_white_24"
+ android:tint="@color/incoming_answer_icon"
+ android:tintMode="src_atop"
+ tools:outlineProvider="background"/>
+
+ </FrameLayout>
+ <TextView
+ android:id="@+id/incoming_swipe_to_reject_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="20dp"
+ android:layout_gravity="center_horizontal"
+ android:alpha="0"
+ android:focusable="false"
+ android:text="@string/call_incoming_swipe_to_reject"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance.Hint"
+ tools:alpha="1"/>
+ </LinearLayout>
+ <FrameLayout
+ android:id="@+id/hint_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false"
+ android:clipToPadding="false"/>
+</FrameLayout>
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/layout/two_button_method.xml b/java/com/android/incallui/answer/impl/answermethod/res/layout/two_button_method.xml
new file mode 100644
index 000000000..f92f3c428
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/layout/two_button_method.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="bottom|center_horizontal"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/two_button_hint_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="32dp"
+ android:accessibilityLiveRegion="polite"
+ android:alpha="0"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/two_button_bottom_padding"
+ android:gravity="bottom|center_horizontal"
+ android:orientation="horizontal">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="88dp"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:padding="@dimen/incall_call_button_elevation"
+ android:gravity="center_horizontal"
+ android:orientation="vertical">
+
+ <ImageButton
+ android:id="@+id/two_button_decline_button"
+ style="@style/Answer.Button.Decline"
+ android:layout_width="@dimen/two_button_button_size"
+ android:layout_height="@dimen/two_button_button_size"
+ android:contentDescription="@string/a11y_call_incoming_decline_description"
+ android:src="@drawable/quantum_ic_call_end_white_24"/>
+
+ <TextView
+ android:id="@+id/two_button_decline_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/two_button_label_padding"
+ android:importantForAccessibility="no"
+ android:text="@string/call_incoming_decline"
+ android:textColor="#ffffffff"
+ android:textSize="@dimen/two_button_label_size"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:padding="@dimen/incall_call_button_elevation"
+ android:gravity="center_horizontal"
+ android:orientation="vertical">
+
+ <ImageButton
+ android:id="@+id/two_button_answer_button"
+ style="@style/Answer.Button.Answer"
+ android:layout_width="@dimen/two_button_button_size"
+ android:layout_height="@dimen/two_button_button_size"
+ android:contentDescription="@string/a11y_call_incoming_answer_description"
+ android:src="@drawable/quantum_ic_call_white_24"/>
+
+ <TextView
+ android:id="@+id/two_button_answer_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/two_button_label_padding"
+ android:importantForAccessibility="no"
+ android:text="@string/call_incoming_answer"
+ android:textColor="#ffffffff"
+ android:textSize="@dimen/two_button_label_size"/>
+
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout>
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/values-h240dp/values.xml b/java/com/android/incallui/answer/impl/answermethod/res/values-h240dp/values.xml
new file mode 100644
index 000000000..7d99b29aa
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/values-h240dp/values.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <bool name="two_button_show_button_labels">true</bool>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/values-h280dp/dimens.xml b/java/com/android/incallui/answer/impl/answermethod/res/values-h280dp/dimens.xml
new file mode 100644
index 000000000..e7e223d8c
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/values-h280dp/dimens.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <dimen name="two_button_button_size">64dp</dimen>
+ <dimen name="two_button_label_padding">16dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/values-h480dp/dimens.xml b/java/com/android/incallui/answer/impl/answermethod/res/values-h480dp/dimens.xml
new file mode 100644
index 000000000..b7b4bd894
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/values-h480dp/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <dimen name="two_button_bottom_padding">60dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/values/dimens.xml b/java/com/android/incallui/answer/impl/answermethod/res/values/dimens.xml
new file mode 100644
index 000000000..bf160f9ac
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/values/dimens.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <dimen name="answer_contact_puck_size_photo">88dp</dimen>
+ <dimen name="answer_contact_puck_size_no_photo">72dp</dimen>
+ <dimen name="two_button_button_size">48dp</dimen>
+ <dimen name="two_button_label_size">12sp</dimen>
+ <dimen name="two_button_label_padding">8dp</dimen>
+ <dimen name="two_button_bottom_padding">24dp</dimen>
+ <dimen name="answer_swipe_dead_zone_sides">50dp</dimen>
+ <dimen name="answer_swipe_dead_zone_top">150dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/values/ids.xml b/java/com/android/incallui/answer/impl/answermethod/res/values/ids.xml
new file mode 100644
index 000000000..fc03cacbd
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/values/ids.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <item name="accessibility_action_answer" type="id"/>
+ <item name="accessibility_action_decline" type="id"/>
+</resources> \ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/values/strings.xml b/java/com/android/incallui/answer/impl/answermethod/res/values/strings.xml
new file mode 100644
index 000000000..8b50dbf1a
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/values/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="call_incoming_swipe_to_answer">Swipe up to answer</string>
+ <string name="call_incoming_swipe_to_reject">Swipe down to reject</string>
+ <string name="a11y_incoming_call_swipe_to_answer">Swipe up with two fingers to answer or down to reject the call</string>
+ <string name="call_incoming_will_disconnect">Answering this call will end your video call</string>
+
+ <string name="a11y_call_incoming_decline_description">Decline</string>
+ <string name="call_incoming_decline">Decline</string>
+
+ <string name="a11y_call_incoming_answer_description">Answer</string>
+ <string name="call_incoming_answer">Answer</string>
+
+</resources>
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/values/styles.xml b/java/com/android/incallui/answer/impl/answermethod/res/values/styles.xml
new file mode 100644
index 000000000..fd3ca7ca0
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/values/styles.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="Dialer.Incall.TextAppearance.Hint">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textStyle">italic</item>
+ </style>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/values/values.xml b/java/com/android/incallui/answer/impl/answermethod/res/values/values.xml
new file mode 100644
index 000000000..43b2cd273
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/values/values.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <color name="incoming_or_outgoing_call_screen_mask">@android:color/transparent</color>
+ <color name="call_hangup_background">#DF0000</color>
+ <color name="call_accept_background">#00C853</color>
+ <color name="incoming_answer_icon">#00C853</color>
+ <integer name="button_exit_fade_delay_ms">300</integer>
+ <bool name="two_button_show_button_labels">false</bool>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/classifier/AccelerationClassifier.java b/java/com/android/incallui/answer/impl/classifier/AccelerationClassifier.java
new file mode 100644
index 000000000..ac504444e
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/AccelerationClassifier.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+import android.util.ArrayMap;
+import android.view.MotionEvent;
+import java.util.Map;
+
+/**
+ * A classifier which looks at the speed and distance between successive points of a Stroke. It
+ * looks at two consecutive speeds between two points and calculates the ratio between them. The
+ * final result is the maximum of these values. It does the same for distances. If some speed or
+ * distance is equal to zero then the ratio between this and the next part is not calculated. To the
+ * duration of each part there is added one nanosecond so that it is always possible to calculate
+ * the speed of a part.
+ */
+class AccelerationClassifier extends StrokeClassifier {
+ private final Map<Stroke, Data> mStrokeMap = new ArrayMap<>();
+
+ public AccelerationClassifier(ClassifierData classifierData) {
+ mClassifierData = classifierData;
+ }
+
+ @Override
+ public String getTag() {
+ return "ACC";
+ }
+
+ @Override
+ public void onTouchEvent(MotionEvent event) {
+ int action = event.getActionMasked();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mStrokeMap.clear();
+ }
+
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ Stroke stroke = mClassifierData.getStroke(event.getPointerId(i));
+ Point point = stroke.getPoints().get(stroke.getPoints().size() - 1);
+ if (mStrokeMap.get(stroke) == null) {
+ mStrokeMap.put(stroke, new Data(point));
+ } else {
+ mStrokeMap.get(stroke).addPoint(point);
+ }
+ }
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ Data data = mStrokeMap.get(stroke);
+ return 2 * SpeedRatioEvaluator.evaluate(data.maxSpeedRatio);
+ }
+
+ private static class Data {
+
+ static final float MILLIS_TO_NANOS = 1e6f;
+
+ Point previousPoint;
+ float previousSpeed = 0;
+ float maxSpeedRatio = 0;
+
+ public Data(Point point) {
+ previousPoint = point;
+ }
+
+ public void addPoint(Point point) {
+ float distance = previousPoint.dist(point);
+ float duration = (float) (point.timeOffsetNano - previousPoint.timeOffsetNano + 1);
+ float speed = distance / duration;
+
+ if (duration > 20 * MILLIS_TO_NANOS || duration < 5 * MILLIS_TO_NANOS) {
+ // reject this segment and ensure we won't use data about it in the next round.
+ previousSpeed = 0;
+ previousPoint = point;
+ return;
+ }
+ if (previousSpeed != 0.0f) {
+ maxSpeedRatio = Math.max(maxSpeedRatio, speed / previousSpeed);
+ }
+
+ previousSpeed = speed;
+ previousPoint = point;
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/AnglesClassifier.java b/java/com/android/incallui/answer/impl/classifier/AnglesClassifier.java
new file mode 100644
index 000000000..dbfbcfc1c
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/AnglesClassifier.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+import android.util.ArrayMap;
+import android.view.MotionEvent;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A classifier which calculates the variance of differences between successive angles in a stroke.
+ * For each stroke it keeps its last three points. If some successive points are the same, it
+ * ignores the repetitions. If a new point is added, the classifier calculates the angle between the
+ * last three points. After that, it calculates the difference between this angle and the previously
+ * calculated angle. Then it calculates the variance of the differences from a stroke. To the
+ * differences there is artificially added value 0.0 and the difference between the first angle and
+ * PI (angles are in radians). It helps with strokes which have few points and punishes more strokes
+ * which are not smooth.
+ *
+ * <p>This classifier also tries to split the stroke into two parts in the place in which the
+ * biggest angle is. It calculates the angle variance of the two parts and sums them up. The reason
+ * the classifier is doing this, is because some human swipes at the beginning go for a moment in
+ * one direction and then they rapidly change direction for the rest of the stroke (like a tick).
+ * The final result is the minimum of angle variance of the whole stroke and the sum of angle
+ * variances of the two parts split up. The classifier tries the tick option only if the first part
+ * is shorter than the second part.
+ *
+ * <p>Additionally, the classifier classifies the angles as left angles (those angles which value is
+ * in [0.0, PI - ANGLE_DEVIATION) interval), straight angles ([PI - ANGLE_DEVIATION, PI +
+ * ANGLE_DEVIATION] interval) and right angles ((PI + ANGLE_DEVIATION, 2 * PI) interval) and then
+ * calculates the percentage of angles which are in the same direction (straight angles can be left
+ * angels or right angles)
+ */
+class AnglesClassifier extends StrokeClassifier {
+ private Map<Stroke, Data> mStrokeMap = new ArrayMap<>();
+
+ public AnglesClassifier(ClassifierData classifierData) {
+ mClassifierData = classifierData;
+ }
+
+ @Override
+ public String getTag() {
+ return "ANG";
+ }
+
+ @Override
+ public void onTouchEvent(MotionEvent event) {
+ int action = event.getActionMasked();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mStrokeMap.clear();
+ }
+
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ Stroke stroke = mClassifierData.getStroke(event.getPointerId(i));
+
+ if (mStrokeMap.get(stroke) == null) {
+ mStrokeMap.put(stroke, new Data());
+ }
+ mStrokeMap.get(stroke).addPoint(stroke.getPoints().get(stroke.getPoints().size() - 1));
+ }
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ Data data = mStrokeMap.get(stroke);
+ return AnglesVarianceEvaluator.evaluate(data.getAnglesVariance())
+ + AnglesPercentageEvaluator.evaluate(data.getAnglesPercentage());
+ }
+
+ private static class Data {
+ private static final float ANGLE_DEVIATION = (float) Math.PI / 20.0f;
+ private static final float MIN_MOVE_DIST_DP = .01f;
+
+ private List<Point> mLastThreePoints = new ArrayList<>();
+ private float mFirstAngleVariance;
+ private float mPreviousAngle;
+ private float mBiggestAngle;
+ private float mSumSquares;
+ private float mSecondSumSquares;
+ private float mSum;
+ private float mSecondSum;
+ private float mCount;
+ private float mSecondCount;
+ private float mFirstLength;
+ private float mLength;
+ private float mAnglesCount;
+ private float mLeftAngles;
+ private float mRightAngles;
+ private float mStraightAngles;
+
+ public Data() {
+ mFirstAngleVariance = 0.0f;
+ mPreviousAngle = (float) Math.PI;
+ mBiggestAngle = 0.0f;
+ mSumSquares = mSecondSumSquares = 0.0f;
+ mSum = mSecondSum = 0.0f;
+ mCount = mSecondCount = 1.0f;
+ mLength = mFirstLength = 0.0f;
+ mAnglesCount = mLeftAngles = mRightAngles = mStraightAngles = 0.0f;
+ }
+
+ public void addPoint(Point point) {
+ // Checking if the added point is different than the previously added point
+ // Repetitions and short distances are being ignored so that proper angles are calculated.
+ if (mLastThreePoints.isEmpty()
+ || (!mLastThreePoints.get(mLastThreePoints.size() - 1).equals(point)
+ && (mLastThreePoints.get(mLastThreePoints.size() - 1).dist(point)
+ > MIN_MOVE_DIST_DP))) {
+ if (!mLastThreePoints.isEmpty()) {
+ mLength += mLastThreePoints.get(mLastThreePoints.size() - 1).dist(point);
+ }
+ mLastThreePoints.add(point);
+ if (mLastThreePoints.size() == 4) {
+ mLastThreePoints.remove(0);
+
+ float angle =
+ mLastThreePoints.get(1).getAngle(mLastThreePoints.get(0), mLastThreePoints.get(2));
+
+ mAnglesCount++;
+ if (angle < Math.PI - ANGLE_DEVIATION) {
+ mLeftAngles++;
+ } else if (angle <= Math.PI + ANGLE_DEVIATION) {
+ mStraightAngles++;
+ } else {
+ mRightAngles++;
+ }
+
+ float difference = angle - mPreviousAngle;
+
+ // If this is the biggest angle of the stroke so then we save the value of
+ // the angle variance so far and start to count the values for the angle
+ // variance of the second part.
+ if (mBiggestAngle < angle) {
+ mBiggestAngle = angle;
+ mFirstLength = mLength;
+ mFirstAngleVariance = getAnglesVariance(mSumSquares, mSum, mCount);
+ mSecondSumSquares = 0.0f;
+ mSecondSum = 0.0f;
+ mSecondCount = 1.0f;
+ } else {
+ mSecondSum += difference;
+ mSecondSumSquares += difference * difference;
+ mSecondCount += 1.0f;
+ }
+
+ mSum += difference;
+ mSumSquares += difference * difference;
+ mCount += 1.0f;
+ mPreviousAngle = angle;
+ }
+ }
+ }
+
+ public float getAnglesVariance(float sumSquares, float sum, float count) {
+ return sumSquares / count - (sum / count) * (sum / count);
+ }
+
+ public float getAnglesVariance() {
+ float anglesVariance = getAnglesVariance(mSumSquares, mSum, mCount);
+ if (mFirstLength < mLength / 2f) {
+ anglesVariance =
+ Math.min(
+ anglesVariance,
+ mFirstAngleVariance
+ + getAnglesVariance(mSecondSumSquares, mSecondSum, mSecondCount));
+ }
+ return anglesVariance;
+ }
+
+ public float getAnglesPercentage() {
+ if (mAnglesCount == 0.0f) {
+ return 1.0f;
+ }
+ return (Math.max(mLeftAngles, mRightAngles) + mStraightAngles) / mAnglesCount;
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/AnglesPercentageEvaluator.java b/java/com/android/incallui/answer/impl/classifier/AnglesPercentageEvaluator.java
new file mode 100644
index 000000000..49a183596
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/AnglesPercentageEvaluator.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+class AnglesPercentageEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value < 1.00) {
+ evaluation++;
+ }
+ if (value < 0.90) {
+ evaluation++;
+ }
+ if (value < 0.70) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/AnglesVarianceEvaluator.java b/java/com/android/incallui/answer/impl/classifier/AnglesVarianceEvaluator.java
new file mode 100644
index 000000000..db4de6a3b
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/AnglesVarianceEvaluator.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+class AnglesVarianceEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value > 0.05) {
+ evaluation++;
+ }
+ if (value > 0.10) {
+ evaluation++;
+ }
+ if (value > 0.20) {
+ evaluation++;
+ }
+ if (value > 0.40) {
+ evaluation++;
+ }
+ if (value > 0.80) {
+ evaluation++;
+ }
+ if (value > 1.50) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/Classifier.java b/java/com/android/incallui/answer/impl/classifier/Classifier.java
new file mode 100644
index 000000000..c6fbff327
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/Classifier.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+import android.hardware.SensorEvent;
+import android.view.MotionEvent;
+
+/** An abstract class for classifiers for touch and sensor events. */
+abstract class Classifier {
+
+ /** Contains all the information about touch events from which the classifier can query */
+ protected ClassifierData mClassifierData;
+
+ /** Informs the classifier that a new touch event has occurred */
+ public void onTouchEvent(MotionEvent event) {}
+
+ /** Informs the classifier that a sensor change occurred */
+ public void onSensorChanged(SensorEvent event) {}
+
+ public abstract String getTag();
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/ClassifierData.java b/java/com/android/incallui/answer/impl/classifier/ClassifierData.java
new file mode 100644
index 000000000..ae07d27a0
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/ClassifierData.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Contains data which is used to classify interaction sequences on the lockscreen. It does, for
+ * example, provide information on the current touch state.
+ */
+class ClassifierData {
+ private SparseArray<Stroke> mCurrentStrokes = new SparseArray<>();
+ private ArrayList<Stroke> mEndingStrokes = new ArrayList<>();
+ private final float mDpi;
+ private final float mScreenHeight;
+
+ public ClassifierData(float dpi, float screenHeight) {
+ mDpi = dpi;
+ mScreenHeight = screenHeight / dpi;
+ }
+
+ public void update(MotionEvent event) {
+ mEndingStrokes.clear();
+ int action = event.getActionMasked();
+ if (action == MotionEvent.ACTION_DOWN) {
+ mCurrentStrokes.clear();
+ }
+
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ int id = event.getPointerId(i);
+ if (mCurrentStrokes.get(id) == null) {
+ // TODO (keyboardr): See if there's a way to use event.getEventTimeNanos() instead
+ mCurrentStrokes.put(
+ id, new Stroke(TimeUnit.MILLISECONDS.toNanos(event.getEventTime()), mDpi));
+ }
+ mCurrentStrokes
+ .get(id)
+ .addPoint(
+ event.getX(i), event.getY(i), TimeUnit.MILLISECONDS.toNanos(event.getEventTime()));
+
+ if (action == MotionEvent.ACTION_UP
+ || action == MotionEvent.ACTION_CANCEL
+ || (action == MotionEvent.ACTION_POINTER_UP && i == event.getActionIndex())) {
+ mEndingStrokes.add(getStroke(id));
+ }
+ }
+ }
+
+ void cleanUp(MotionEvent event) {
+ mEndingStrokes.clear();
+ int action = event.getActionMasked();
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ int id = event.getPointerId(i);
+ if (action == MotionEvent.ACTION_UP
+ || action == MotionEvent.ACTION_CANCEL
+ || (action == MotionEvent.ACTION_POINTER_UP && i == event.getActionIndex())) {
+ mCurrentStrokes.remove(id);
+ }
+ }
+ }
+
+ /** @return the list of Strokes which are ending in the recently added MotionEvent */
+ public ArrayList<Stroke> getEndingStrokes() {
+ return mEndingStrokes;
+ }
+
+ /**
+ * @param id the id from MotionEvent
+ * @return the Stroke assigned to the id
+ */
+ public Stroke getStroke(int id) {
+ return mCurrentStrokes.get(id);
+ }
+
+ /** @return the height of the screen in inches */
+ public float getScreenHeight() {
+ return mScreenHeight;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/DirectionClassifier.java b/java/com/android/incallui/answer/impl/classifier/DirectionClassifier.java
new file mode 100644
index 000000000..068626859
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/DirectionClassifier.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+/**
+ * A classifier which looks at the general direction of a stroke and evaluates it depending on the
+ * type of action that takes place.
+ */
+public class DirectionClassifier extends StrokeClassifier {
+ public DirectionClassifier(ClassifierData classifierData) {}
+
+ @Override
+ public String getTag() {
+ return "DIR";
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ Point firstPoint = stroke.getPoints().get(0);
+ Point lastPoint = stroke.getPoints().get(stroke.getPoints().size() - 1);
+ return DirectionEvaluator.evaluate(lastPoint.x - firstPoint.x, lastPoint.y - firstPoint.y);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/DirectionEvaluator.java b/java/com/android/incallui/answer/impl/classifier/DirectionEvaluator.java
new file mode 100644
index 000000000..cdc1cfe1e
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/DirectionEvaluator.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+class DirectionEvaluator {
+ public static float evaluate(float xDiff, float yDiff) {
+ return Math.abs(yDiff) < Math.abs(xDiff) ? 5.5f : 0.0f;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/DurationCountClassifier.java b/java/com/android/incallui/answer/impl/classifier/DurationCountClassifier.java
new file mode 100644
index 000000000..0b9f1138d
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/DurationCountClassifier.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+/**
+ * A classifier which looks at the ratio between the duration of the stroke and its number of
+ * points.
+ */
+class DurationCountClassifier extends StrokeClassifier {
+ public DurationCountClassifier(ClassifierData classifierData) {}
+
+ @Override
+ public String getTag() {
+ return "DUR";
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ return DurationCountEvaluator.evaluate(stroke.getDurationSeconds() / stroke.getCount());
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/DurationCountEvaluator.java b/java/com/android/incallui/answer/impl/classifier/DurationCountEvaluator.java
new file mode 100644
index 000000000..5b232fe95
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/DurationCountEvaluator.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+class DurationCountEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value < 0.0105) {
+ evaluation++;
+ }
+ if (value < 0.00909) {
+ evaluation++;
+ }
+ if (value < 0.00667) {
+ evaluation++;
+ }
+ if (value > 0.0333) {
+ evaluation++;
+ }
+ if (value > 0.0500) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/EndPointLengthClassifier.java b/java/com/android/incallui/answer/impl/classifier/EndPointLengthClassifier.java
new file mode 100644
index 000000000..95b317638
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/EndPointLengthClassifier.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+/**
+ * A classifier which looks at the distance between the first and the last point from the stroke.
+ */
+class EndPointLengthClassifier extends StrokeClassifier {
+ public EndPointLengthClassifier(ClassifierData classifierData) {
+ mClassifierData = classifierData;
+ }
+
+ @Override
+ public String getTag() {
+ return "END_LNGTH";
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ return EndPointLengthEvaluator.evaluate(stroke.getEndPointLength());
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/EndPointLengthEvaluator.java b/java/com/android/incallui/answer/impl/classifier/EndPointLengthEvaluator.java
new file mode 100644
index 000000000..74bfffba4
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/EndPointLengthEvaluator.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+class EndPointLengthEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value < 0.05) {
+ evaluation += 2.0f;
+ }
+ if (value < 0.1) {
+ evaluation += 2.0f;
+ }
+ if (value < 0.2) {
+ evaluation += 2.0f;
+ }
+ if (value < 0.3) {
+ evaluation += 2.0f;
+ }
+ if (value < 0.4) {
+ evaluation += 2.0f;
+ }
+ if (value < 0.5) {
+ evaluation += 2.0f;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/EndPointRatioClassifier.java b/java/com/android/incallui/answer/impl/classifier/EndPointRatioClassifier.java
new file mode 100644
index 000000000..01a35c126
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/EndPointRatioClassifier.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+/**
+ * A classifier which looks at the ratio between the total length covered by the stroke and the
+ * distance between the first and last point from this stroke.
+ */
+class EndPointRatioClassifier extends StrokeClassifier {
+ public EndPointRatioClassifier(ClassifierData classifierData) {
+ mClassifierData = classifierData;
+ }
+
+ @Override
+ public String getTag() {
+ return "END_RTIO";
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ float ratio;
+ if (stroke.getTotalLength() == 0.0f) {
+ ratio = 1.0f;
+ } else {
+ ratio = stroke.getEndPointLength() / stroke.getTotalLength();
+ }
+ return EndPointRatioEvaluator.evaluate(ratio);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/EndPointRatioEvaluator.java b/java/com/android/incallui/answer/impl/classifier/EndPointRatioEvaluator.java
new file mode 100644
index 000000000..1d64bea8e
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/EndPointRatioEvaluator.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+class EndPointRatioEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value < 0.85) {
+ evaluation++;
+ }
+ if (value < 0.75) {
+ evaluation++;
+ }
+ if (value < 0.65) {
+ evaluation++;
+ }
+ if (value < 0.55) {
+ evaluation++;
+ }
+ if (value < 0.45) {
+ evaluation++;
+ }
+ if (value < 0.35) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/FalsingManager.java b/java/com/android/incallui/answer/impl/classifier/FalsingManager.java
new file mode 100644
index 000000000..fdcc0a3f9
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/FalsingManager.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.PowerManager;
+import android.view.MotionEvent;
+import android.view.accessibility.AccessibilityManager;
+
+/**
+ * When the phone is locked, listens to touch, sensor and phone events and sends them to
+ * HumanInteractionClassifier to determine if touches are coming from a human.
+ */
+public class FalsingManager implements SensorEventListener {
+ private static final int[] CLASSIFIER_SENSORS =
+ new int[] {
+ Sensor.TYPE_PROXIMITY,
+ };
+
+ private final SensorManager mSensorManager;
+ private final HumanInteractionClassifier mHumanInteractionClassifier;
+ private final AccessibilityManager mAccessibilityManager;
+
+ private boolean mSessionActive = false;
+ private boolean mScreenOn;
+
+ public FalsingManager(Context context) {
+ mSensorManager = context.getSystemService(SensorManager.class);
+ mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
+ mHumanInteractionClassifier = new HumanInteractionClassifier(context);
+ mScreenOn = context.getSystemService(PowerManager.class).isInteractive();
+ }
+
+ /** Returns {@code true} iff the FalsingManager is enabled and able to classify touches */
+ public boolean isEnabled() {
+ return mHumanInteractionClassifier.isEnabled();
+ }
+
+ /**
+ * Returns {@code true} iff the classifier determined that this is not a human interacting with
+ * the phone.
+ */
+ public boolean isFalseTouch() {
+ // Touch exploration triggers false positives in the classifier and
+ // already sufficiently prevents false unlocks.
+ return !mAccessibilityManager.isTouchExplorationEnabled()
+ && mHumanInteractionClassifier.isFalseTouch();
+ }
+
+ /**
+ * Should be called when the screen turns on and the related Views become visible. This will start
+ * tracking changes if the manager is enabled.
+ */
+ public void onScreenOn() {
+ mScreenOn = true;
+ sessionEntrypoint();
+ }
+
+ /**
+ * Should be called when the screen turns off or the related Views are no longer visible. This
+ * will cause the manager to stop tracking changes.
+ */
+ public void onScreenOff() {
+ mScreenOn = false;
+ sessionExitpoint();
+ }
+
+ /**
+ * Should be called when a new touch event has been received and should be classified.
+ *
+ * @param event MotionEvent to be classified as human or false.
+ */
+ public void onTouchEvent(MotionEvent event) {
+ if (mSessionActive) {
+ mHumanInteractionClassifier.onTouchEvent(event);
+ }
+ }
+
+ @Override
+ public synchronized void onSensorChanged(SensorEvent event) {
+ mHumanInteractionClassifier.onSensorChanged(event);
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {}
+
+ private boolean shouldSessionBeActive() {
+ return isEnabled() && mScreenOn;
+ }
+
+ private boolean sessionEntrypoint() {
+ if (!mSessionActive && shouldSessionBeActive()) {
+ onSessionStart();
+ return true;
+ }
+ return false;
+ }
+
+ private void sessionExitpoint() {
+ if (mSessionActive && !shouldSessionBeActive()) {
+ mSessionActive = false;
+ mSensorManager.unregisterListener(this);
+ }
+ }
+
+ private void onSessionStart() {
+ mSessionActive = true;
+
+ if (mHumanInteractionClassifier.isEnabled()) {
+ registerSensors(CLASSIFIER_SENSORS);
+ }
+ }
+
+ private void registerSensors(int[] sensors) {
+ for (int sensorType : sensors) {
+ Sensor s = mSensorManager.getDefaultSensor(sensorType);
+ if (s != null) {
+ mSensorManager.registerListener(this, s, SensorManager.SENSOR_DELAY_GAME);
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/GestureClassifier.java b/java/com/android/incallui/answer/impl/classifier/GestureClassifier.java
new file mode 100644
index 000000000..afd7ea0e7
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/GestureClassifier.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+/**
+ * An abstract class for classifiers which classify the whole gesture (all the strokes which
+ * occurred from DOWN event to UP/CANCEL event)
+ */
+abstract class GestureClassifier extends Classifier {
+
+ /**
+ * @return a non-negative value which is used to determine whether the most recent gesture is a
+ * false interaction; the bigger the value the greater the chance that this a false
+ * interaction.
+ */
+ public abstract float getFalseTouchEvaluation();
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/HistoryEvaluator.java b/java/com/android/incallui/answer/impl/classifier/HistoryEvaluator.java
new file mode 100644
index 000000000..3f302c65f
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/HistoryEvaluator.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+import android.os.SystemClock;
+
+import java.util.ArrayList;
+
+/**
+ * Holds the evaluations for ended strokes and gestures. These values are decreased through time.
+ */
+class HistoryEvaluator {
+ private static final float INTERVAL = 50.0f;
+ private static final float HISTORY_FACTOR = 0.9f;
+ private static final float EPSILON = 1e-5f;
+
+ private final ArrayList<Data> mStrokes = new ArrayList<>();
+ private final ArrayList<Data> mGestureWeights = new ArrayList<>();
+ private long mLastUpdate;
+
+ public HistoryEvaluator() {
+ mLastUpdate = SystemClock.elapsedRealtime();
+ }
+
+ public void addStroke(float evaluation) {
+ decayValue();
+ mStrokes.add(new Data(evaluation));
+ }
+
+ public void addGesture(float evaluation) {
+ decayValue();
+ mGestureWeights.add(new Data(evaluation));
+ }
+
+ /** Calculates the weighted average of strokes and adds to it the weighted average of gestures */
+ public float getEvaluation() {
+ return weightedAverage(mStrokes) + weightedAverage(mGestureWeights);
+ }
+
+ private float weightedAverage(ArrayList<Data> list) {
+ float sumValue = 0.0f;
+ float sumWeight = 0.0f;
+ int size = list.size();
+ for (int i = 0; i < size; i++) {
+ Data data = list.get(i);
+ sumValue += data.evaluation * data.weight;
+ sumWeight += data.weight;
+ }
+
+ if (sumWeight == 0.0f) {
+ return 0.0f;
+ }
+
+ return sumValue / sumWeight;
+ }
+
+ private void decayValue() {
+ long time = SystemClock.elapsedRealtime();
+
+ if (time <= mLastUpdate) {
+ return;
+ }
+
+ // All weights are multiplied by HISTORY_FACTOR after each INTERVAL milliseconds.
+ float factor = (float) Math.pow(HISTORY_FACTOR, (time - mLastUpdate) / INTERVAL);
+
+ decayValue(mStrokes, factor);
+ decayValue(mGestureWeights, factor);
+ mLastUpdate = time;
+ }
+
+ private void decayValue(ArrayList<Data> list, float factor) {
+ int size = list.size();
+ for (int i = 0; i < size; i++) {
+ list.get(i).weight *= factor;
+ }
+
+ // Removing evaluations with such small weights that they do not matter anymore
+ while (!list.isEmpty() && isZero(list.get(0).weight)) {
+ list.remove(0);
+ }
+ }
+
+ private boolean isZero(float x) {
+ return x <= EPSILON && x >= -EPSILON;
+ }
+
+ /**
+ * For each stroke it holds its initial value and the current weight. Initially the weight is set
+ * to 1.0
+ */
+ private static class Data {
+ public float evaluation;
+ public float weight;
+
+ public Data(float evaluation) {
+ this.evaluation = evaluation;
+ weight = 1.0f;
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/HumanInteractionClassifier.java b/java/com/android/incallui/answer/impl/classifier/HumanInteractionClassifier.java
new file mode 100644
index 000000000..1d3d7ef22
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/HumanInteractionClassifier.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+import android.content.Context;
+import android.hardware.SensorEvent;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import com.android.dialer.common.ConfigProviderBindings;
+
+/** An classifier trying to determine whether it is a human interacting with the phone or not. */
+class HumanInteractionClassifier extends Classifier {
+
+ private static final String CONFIG_ANSWER_FALSE_TOUCH_DETECTION_ENABLED =
+ "answer_false_touch_detection_enabled";
+
+ private final StrokeClassifier[] mStrokeClassifiers;
+ private final GestureClassifier[] mGestureClassifiers;
+ private final HistoryEvaluator mHistoryEvaluator;
+ private final boolean mEnabled;
+
+ HumanInteractionClassifier(Context context) {
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+
+ // If the phone is rotated to landscape, the calculations would be wrong if xdpi and ydpi
+ // were to be used separately. Due negligible differences in xdpi and ydpi we can just
+ // take the average.
+ // Note that xdpi and ydpi are the physical pixels per inch and are not affected by scaling.
+ float dpi = (displayMetrics.xdpi + displayMetrics.ydpi) / 2.0f;
+ mClassifierData = new ClassifierData(dpi, displayMetrics.heightPixels);
+ mHistoryEvaluator = new HistoryEvaluator();
+ mEnabled =
+ ConfigProviderBindings.get(context)
+ .getBoolean(CONFIG_ANSWER_FALSE_TOUCH_DETECTION_ENABLED, true);
+
+ mStrokeClassifiers =
+ new StrokeClassifier[] {
+ new AnglesClassifier(mClassifierData),
+ new SpeedClassifier(mClassifierData),
+ new DurationCountClassifier(mClassifierData),
+ new EndPointRatioClassifier(mClassifierData),
+ new EndPointLengthClassifier(mClassifierData),
+ new AccelerationClassifier(mClassifierData),
+ new SpeedAnglesClassifier(mClassifierData),
+ new LengthCountClassifier(mClassifierData),
+ new DirectionClassifier(mClassifierData)
+ };
+
+ mGestureClassifiers =
+ new GestureClassifier[] {
+ new PointerCountClassifier(mClassifierData), new ProximityClassifier(mClassifierData)
+ };
+ }
+
+ @Override
+ public void onTouchEvent(MotionEvent event) {
+
+ // If the user is dragging down the notification, they might want to drag it down
+ // enough to see the content, read it for a while and then lift the finger to open
+ // the notification. This kind of motion scores very bad in the Classifier so the
+ // MotionEvents which are close to the current position of the finger are not
+ // sent to the classifiers until the finger moves far enough. When the finger if lifted
+ // up, the last MotionEvent which was far enough from the finger is set as the final
+ // MotionEvent and sent to the Classifiers.
+ addTouchEvent(event);
+ }
+
+ private void addTouchEvent(MotionEvent event) {
+ mClassifierData.update(event);
+
+ for (StrokeClassifier c : mStrokeClassifiers) {
+ c.onTouchEvent(event);
+ }
+
+ for (GestureClassifier c : mGestureClassifiers) {
+ c.onTouchEvent(event);
+ }
+
+ int size = mClassifierData.getEndingStrokes().size();
+ for (int i = 0; i < size; i++) {
+ Stroke stroke = mClassifierData.getEndingStrokes().get(i);
+ float evaluation = 0.0f;
+ for (StrokeClassifier c : mStrokeClassifiers) {
+ float e = c.getFalseTouchEvaluation(stroke);
+ evaluation += e;
+ }
+
+ mHistoryEvaluator.addStroke(evaluation);
+ }
+
+ int action = event.getActionMasked();
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ float evaluation = 0.0f;
+ for (GestureClassifier c : mGestureClassifiers) {
+ float e = c.getFalseTouchEvaluation();
+ evaluation += e;
+ }
+ mHistoryEvaluator.addGesture(evaluation);
+ }
+
+ mClassifierData.cleanUp(event);
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ for (Classifier c : mStrokeClassifiers) {
+ c.onSensorChanged(event);
+ }
+
+ for (Classifier c : mGestureClassifiers) {
+ c.onSensorChanged(event);
+ }
+ }
+
+ boolean isFalseTouch() {
+ float evaluation = mHistoryEvaluator.getEvaluation();
+ return evaluation >= 5.0f;
+ }
+
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ @Override
+ public String getTag() {
+ return "HIC";
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/LengthCountClassifier.java b/java/com/android/incallui/answer/impl/classifier/LengthCountClassifier.java
new file mode 100644
index 000000000..7dd2ab674
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/LengthCountClassifier.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+/**
+ * A classifier which looks at the ratio between the length of the stroke and its number of points.
+ * The number of points is subtracted by 2 because the UP event comes in with some delay and it
+ * should not influence the ratio and also strokes which are long and have a small number of points
+ * are punished more (these kind of strokes are usually bad ones and they tend to score well in
+ * other classifiers).
+ */
+class LengthCountClassifier extends StrokeClassifier {
+ public LengthCountClassifier(ClassifierData classifierData) {}
+
+ @Override
+ public String getTag() {
+ return "LEN_CNT";
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ return LengthCountEvaluator.evaluate(
+ stroke.getTotalLength() / Math.max(1.0f, stroke.getCount() - 2));
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/LengthCountEvaluator.java b/java/com/android/incallui/answer/impl/classifier/LengthCountEvaluator.java
new file mode 100644
index 000000000..2a2225a00
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/LengthCountEvaluator.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+/**
+ * A classifier which looks at the ratio between the length of the stroke and its number of points.
+ */
+class LengthCountEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value < 0.09) {
+ evaluation++;
+ }
+ if (value < 0.05) {
+ evaluation++;
+ }
+ if (value < 0.02) {
+ evaluation++;
+ }
+ if (value > 0.6) {
+ evaluation++;
+ }
+ if (value > 0.9) {
+ evaluation++;
+ }
+ if (value > 1.2) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/Point.java b/java/com/android/incallui/answer/impl/classifier/Point.java
new file mode 100644
index 000000000..5ea48b4ce
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/Point.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+class Point {
+ public float x;
+ public float y;
+ public long timeOffsetNano;
+
+ public Point(float x, float y) {
+ this.x = x;
+ this.y = y;
+ this.timeOffsetNano = 0;
+ }
+
+ public Point(float x, float y, long timeOffsetNano) {
+ this.x = x;
+ this.y = y;
+ this.timeOffsetNano = timeOffsetNano;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof Point)) {
+ return false;
+ }
+ Point otherPoint = ((Point) other);
+ return x == otherPoint.x && y == otherPoint.y;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (x != +0.0f ? Float.floatToIntBits(x) : 0);
+ result = 31 * result + (y != +0.0f ? Float.floatToIntBits(y) : 0);
+ return result;
+ }
+
+ public float dist(Point a) {
+ return (float) Math.hypot(a.x - x, a.y - y);
+ }
+
+ /**
+ * Calculates the cross product of vec(this, a) and vec(this, b) where vec(x,y) is the vector from
+ * point x to point y
+ */
+ public float crossProduct(Point a, Point b) {
+ return (a.x - x) * (b.y - y) - (a.y - y) * (b.x - x);
+ }
+
+ /**
+ * Calculates the dot product of vec(this, a) and vec(this, b) where vec(x,y) is the vector from
+ * point x to point y
+ */
+ public float dotProduct(Point a, Point b) {
+ return (a.x - x) * (b.x - x) + (a.y - y) * (b.y - y);
+ }
+
+ /**
+ * Calculates the angle in radians created by points (a, this, b). If any two of these points are
+ * the same, the method will return 0.0f
+ *
+ * @return the angle in radians
+ */
+ public float getAngle(Point a, Point b) {
+ float dist1 = dist(a);
+ float dist2 = dist(b);
+
+ if (dist1 == 0.0f || dist2 == 0.0f) {
+ return 0.0f;
+ }
+
+ float crossProduct = crossProduct(a, b);
+ float dotProduct = dotProduct(a, b);
+ float cos = Math.min(1.0f, Math.max(-1.0f, dotProduct / dist1 / dist2));
+ float angle = (float) Math.acos(cos);
+ if (crossProduct < 0.0) {
+ angle = 2.0f * (float) Math.PI - angle;
+ }
+ return angle;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/PointerCountClassifier.java b/java/com/android/incallui/answer/impl/classifier/PointerCountClassifier.java
new file mode 100644
index 000000000..070de6c9b
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/PointerCountClassifier.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+import android.view.MotionEvent;
+
+/** A classifier which looks at the total number of traces in the whole gesture. */
+class PointerCountClassifier extends GestureClassifier {
+ private int mCount;
+
+ public PointerCountClassifier(ClassifierData classifierData) {
+ mCount = 0;
+ }
+
+ @Override
+ public String getTag() {
+ return "PTR_CNT";
+ }
+
+ @Override
+ public void onTouchEvent(MotionEvent event) {
+ int action = event.getActionMasked();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mCount = 1;
+ }
+
+ if (action == MotionEvent.ACTION_POINTER_DOWN) {
+ ++mCount;
+ }
+ }
+
+ @Override
+ public float getFalseTouchEvaluation() {
+ return PointerCountEvaluator.evaluate(mCount);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/PointerCountEvaluator.java b/java/com/android/incallui/answer/impl/classifier/PointerCountEvaluator.java
new file mode 100644
index 000000000..aa972da8c
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/PointerCountEvaluator.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+class PointerCountEvaluator {
+ public static float evaluate(int value) {
+ return (value - 1) * (value - 1);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/ProximityClassifier.java b/java/com/android/incallui/answer/impl/classifier/ProximityClassifier.java
new file mode 100644
index 000000000..28701ea6d
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/ProximityClassifier.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.view.MotionEvent;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A classifier which looks at the proximity sensor during the gesture. It calculates the percentage
+ * the proximity sensor showing the near state during the whole gesture
+ */
+class ProximityClassifier extends GestureClassifier {
+ private long mGestureStartTimeNano;
+ private long mNearStartTimeNano;
+ private long mNearDuration;
+ private boolean mNear;
+ private float mAverageNear;
+
+ public ProximityClassifier(ClassifierData classifierData) {}
+
+ @Override
+ public String getTag() {
+ return "PROX";
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ if (event.sensor.getType() == Sensor.TYPE_PROXIMITY) {
+ update(event.values[0] < event.sensor.getMaximumRange(), event.timestamp);
+ }
+ }
+
+ @Override
+ public void onTouchEvent(MotionEvent event) {
+ int action = event.getActionMasked();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mGestureStartTimeNano = TimeUnit.MILLISECONDS.toNanos(event.getEventTime());
+ mNearStartTimeNano = TimeUnit.MILLISECONDS.toNanos(event.getEventTime());
+ mNearDuration = 0;
+ }
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ update(mNear, TimeUnit.MILLISECONDS.toNanos(event.getEventTime()));
+ long duration = TimeUnit.MILLISECONDS.toNanos(event.getEventTime()) - mGestureStartTimeNano;
+
+ if (duration == 0) {
+ mAverageNear = mNear ? 1.0f : 0.0f;
+ } else {
+ mAverageNear = (float) mNearDuration / (float) duration;
+ }
+ }
+ }
+
+ /**
+ * @param near is the sensor showing the near state right now
+ * @param timestampNano time of this event in nanoseconds
+ */
+ private void update(boolean near, long timestampNano) {
+ // This if is necessary because MotionEvents and SensorEvents do not come in
+ // chronological order
+ if (timestampNano > mNearStartTimeNano) {
+ // if the state before was near then add the difference of the current time and
+ // mNearStartTimeNano to mNearDuration.
+ if (mNear) {
+ mNearDuration += timestampNano - mNearStartTimeNano;
+ }
+
+ // if the new state is near, set mNearStartTimeNano equal to this moment.
+ if (near) {
+ mNearStartTimeNano = timestampNano;
+ }
+ }
+ mNear = near;
+ }
+
+ @Override
+ public float getFalseTouchEvaluation() {
+ return ProximityEvaluator.evaluate(mAverageNear);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/ProximityEvaluator.java b/java/com/android/incallui/answer/impl/classifier/ProximityEvaluator.java
new file mode 100644
index 000000000..14636c644
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/ProximityEvaluator.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+class ProximityEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ float threshold = 0.1f;
+ if (value >= threshold) {
+ evaluation += 2.0f;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/SpeedAnglesClassifier.java b/java/com/android/incallui/answer/impl/classifier/SpeedAnglesClassifier.java
new file mode 100644
index 000000000..36ae3ad7c
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/SpeedAnglesClassifier.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+import android.util.ArrayMap;
+import android.view.MotionEvent;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A classifier which for each point from a stroke, it creates a point on plane with coordinates
+ * (timeOffsetNano, distanceCoveredUpToThisPoint) (scaled by DURATION_SCALE and LENGTH_SCALE) and
+ * then it calculates the angle variance of these points like the class {@link AnglesClassifier}
+ * (without splitting it into two parts). The classifier ignores the last point of a stroke because
+ * the UP event comes in with some delay and this ruins the smoothness of this curve. Additionally,
+ * the classifier classifies calculates the percentage of angles which value is in [PI -
+ * ANGLE_DEVIATION, 2* PI) interval. The reason why the classifier does that is because the speed of
+ * a good stroke is most often increases, so most of these angels should be in this interval.
+ */
+class SpeedAnglesClassifier extends StrokeClassifier {
+ private Map<Stroke, Data> mStrokeMap = new ArrayMap<>();
+
+ public SpeedAnglesClassifier(ClassifierData classifierData) {
+ mClassifierData = classifierData;
+ }
+
+ @Override
+ public String getTag() {
+ return "SPD_ANG";
+ }
+
+ @Override
+ public void onTouchEvent(MotionEvent event) {
+ int action = event.getActionMasked();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mStrokeMap.clear();
+ }
+
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ Stroke stroke = mClassifierData.getStroke(event.getPointerId(i));
+
+ if (mStrokeMap.get(stroke) == null) {
+ mStrokeMap.put(stroke, new Data());
+ }
+
+ if (action != MotionEvent.ACTION_UP
+ && action != MotionEvent.ACTION_CANCEL
+ && !(action == MotionEvent.ACTION_POINTER_UP && i == event.getActionIndex())) {
+ mStrokeMap.get(stroke).addPoint(stroke.getPoints().get(stroke.getPoints().size() - 1));
+ }
+ }
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ Data data = mStrokeMap.get(stroke);
+ return SpeedVarianceEvaluator.evaluate(data.getAnglesVariance())
+ + SpeedAnglesPercentageEvaluator.evaluate(data.getAnglesPercentage());
+ }
+
+ private static class Data {
+ private static final float DURATION_SCALE = 1e8f;
+ private static final float LENGTH_SCALE = 1.0f;
+ private static final float ANGLE_DEVIATION = (float) Math.PI / 10.0f;
+
+ private List<Point> mLastThreePoints = new ArrayList<>();
+ private Point mPreviousPoint;
+ private float mPreviousAngle;
+ private float mSumSquares;
+ private float mSum;
+ private float mCount;
+ private float mDist;
+ private float mAnglesCount;
+ private float mAcceleratingAngles;
+
+ public Data() {
+ mPreviousPoint = null;
+ mPreviousAngle = (float) Math.PI;
+ mSumSquares = 0.0f;
+ mSum = 0.0f;
+ mCount = 1.0f;
+ mDist = 0.0f;
+ mAnglesCount = mAcceleratingAngles = 0.0f;
+ }
+
+ public void addPoint(Point point) {
+ if (mPreviousPoint != null) {
+ mDist += mPreviousPoint.dist(point);
+ }
+
+ mPreviousPoint = point;
+ Point speedPoint =
+ new Point((float) point.timeOffsetNano / DURATION_SCALE, mDist / LENGTH_SCALE);
+
+ // Checking if the added point is different than the previously added point
+ // Repetitions are being ignored so that proper angles are calculated.
+ if (mLastThreePoints.isEmpty()
+ || !mLastThreePoints.get(mLastThreePoints.size() - 1).equals(speedPoint)) {
+ mLastThreePoints.add(speedPoint);
+ if (mLastThreePoints.size() == 4) {
+ mLastThreePoints.remove(0);
+
+ float angle =
+ mLastThreePoints.get(1).getAngle(mLastThreePoints.get(0), mLastThreePoints.get(2));
+
+ mAnglesCount++;
+ if (angle >= (float) Math.PI - ANGLE_DEVIATION) {
+ mAcceleratingAngles++;
+ }
+
+ float difference = angle - mPreviousAngle;
+ mSum += difference;
+ mSumSquares += difference * difference;
+ mCount += 1.0f;
+ mPreviousAngle = angle;
+ }
+ }
+ }
+
+ public float getAnglesVariance() {
+ return mSumSquares / mCount - (mSum / mCount) * (mSum / mCount);
+ }
+
+ public float getAnglesPercentage() {
+ if (mAnglesCount == 0.0f) {
+ return 1.0f;
+ }
+ return (mAcceleratingAngles) / mAnglesCount;
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/SpeedAnglesPercentageEvaluator.java b/java/com/android/incallui/answer/impl/classifier/SpeedAnglesPercentageEvaluator.java
new file mode 100644
index 000000000..5a8bc3556
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/SpeedAnglesPercentageEvaluator.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+class SpeedAnglesPercentageEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value < 1.00) {
+ evaluation++;
+ }
+ if (value < 0.90) {
+ evaluation++;
+ }
+ if (value < 0.70) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/SpeedClassifier.java b/java/com/android/incallui/answer/impl/classifier/SpeedClassifier.java
new file mode 100644
index 000000000..f3ade3f49
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/SpeedClassifier.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+/**
+ * A classifier that looks at the speed of the stroke. It calculates the speed of a stroke in inches
+ * per second.
+ */
+class SpeedClassifier extends StrokeClassifier {
+
+ public SpeedClassifier(ClassifierData classifierData) {}
+
+ @Override
+ public String getTag() {
+ return "SPD";
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ float duration = stroke.getDurationSeconds();
+ if (duration == 0.0f) {
+ return SpeedEvaluator.evaluate(0.0f);
+ }
+ return SpeedEvaluator.evaluate(stroke.getTotalLength() / duration);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/SpeedEvaluator.java b/java/com/android/incallui/answer/impl/classifier/SpeedEvaluator.java
new file mode 100644
index 000000000..4f9aace0e
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/SpeedEvaluator.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+class SpeedEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value < 4.0) {
+ evaluation++;
+ }
+ if (value < 2.2) {
+ evaluation++;
+ }
+ if (value > 35.0) {
+ evaluation++;
+ }
+ if (value > 50.0) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/SpeedRatioEvaluator.java b/java/com/android/incallui/answer/impl/classifier/SpeedRatioEvaluator.java
new file mode 100644
index 000000000..7ae111313
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/SpeedRatioEvaluator.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+class SpeedRatioEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value == 0) {
+ return 0;
+ }
+ if (value <= 1.0) {
+ evaluation++;
+ }
+ if (value <= 0.5) {
+ evaluation++;
+ }
+ if (value > 9.0) {
+ evaluation++;
+ }
+ if (value > 18.0) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/SpeedVarianceEvaluator.java b/java/com/android/incallui/answer/impl/classifier/SpeedVarianceEvaluator.java
new file mode 100644
index 000000000..211650cbb
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/SpeedVarianceEvaluator.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+class SpeedVarianceEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value > 0.06) {
+ evaluation++;
+ }
+ if (value > 0.15) {
+ evaluation++;
+ }
+ if (value > 0.3) {
+ evaluation++;
+ }
+ if (value > 0.6) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/Stroke.java b/java/com/android/incallui/answer/impl/classifier/Stroke.java
new file mode 100644
index 000000000..c542d0f7c
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/Stroke.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+import java.util.ArrayList;
+
+/**
+ * Contains data about a stroke (a single trace, all the events from a given id from the
+ * DOWN/POINTER_DOWN event till the UP/POINTER_UP/CANCEL event.)
+ */
+class Stroke {
+
+ private static final float NANOS_TO_SECONDS = 1e9f;
+
+ private ArrayList<Point> mPoints = new ArrayList<>();
+ private long mStartTimeNano;
+ private long mEndTimeNano;
+ private float mLength;
+ private final float mDpi;
+
+ public Stroke(long eventTimeNano, float dpi) {
+ mDpi = dpi;
+ mStartTimeNano = mEndTimeNano = eventTimeNano;
+ }
+
+ public void addPoint(float x, float y, long eventTimeNano) {
+ mEndTimeNano = eventTimeNano;
+ Point point = new Point(x / mDpi, y / mDpi, eventTimeNano - mStartTimeNano);
+ if (!mPoints.isEmpty()) {
+ mLength += mPoints.get(mPoints.size() - 1).dist(point);
+ }
+ mPoints.add(point);
+ }
+
+ public int getCount() {
+ return mPoints.size();
+ }
+
+ public float getTotalLength() {
+ return mLength;
+ }
+
+ public float getEndPointLength() {
+ return mPoints.get(0).dist(mPoints.get(mPoints.size() - 1));
+ }
+
+ public long getDurationNanos() {
+ return mEndTimeNano - mStartTimeNano;
+ }
+
+ public float getDurationSeconds() {
+ return (float) getDurationNanos() / NANOS_TO_SECONDS;
+ }
+
+ public ArrayList<Point> getPoints() {
+ return mPoints;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/StrokeClassifier.java b/java/com/android/incallui/answer/impl/classifier/StrokeClassifier.java
new file mode 100644
index 000000000..8abd7e2ec
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/StrokeClassifier.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.classifier;
+
+/** An abstract class for classifiers which classify each stroke separately. */
+abstract class StrokeClassifier extends Classifier {
+
+ /**
+ * @param stroke the stroke for which the evaluation will be calculated
+ * @return a non-negative value which is used to determine whether this a false touch; the bigger
+ * the value the greater the chance that this a false touch
+ */
+ public abstract float getFalseTouchEvaluation(Stroke stroke);
+}
diff --git a/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml b/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml
new file mode 100644
index 000000000..b5fa6da8f
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml
@@ -0,0 +1,13 @@
+<manifest
+ package="com.android.incallui.answer.impl.hint"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <application>
+ <receiver android:name=".EventSecretCodeListener">
+ <intent-filter>
+ <action android:name="android.provider.Telephony.SECRET_CODE" />
+ <data android:scheme="android_secret_code" />
+ </intent-filter>
+ </receiver>
+ </application>
+</manifest>
diff --git a/java/com/android/incallui/answer/impl/hint/AnswerHint.java b/java/com/android/incallui/answer/impl/hint/AnswerHint.java
new file mode 100644
index 000000000..dd3b8228a
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/AnswerHint.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.hint;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+/** Interface to overlay a hint of how to answer the call. */
+public interface AnswerHint {
+
+ /**
+ * Inflates the hint's layout into the container.
+ *
+ * <p>TODO: if the hint becomes more dependent on other UI elements of the AnswerFragment,
+ * should put put and hintText into another data structure.
+ */
+ void onCreateView(LayoutInflater inflater, ViewGroup container, View puck, TextView hintText);
+
+ /** Called when the puck bounce animation begins. */
+ void onBounceStart();
+
+ /**
+ * Called when the bounce animation has ended (transitioned into other animations). The hint
+ * should reset itself.
+ */
+ void onBounceEnd();
+
+ /** Called when the call is accepted or rejected through user interaction. */
+ void onAnswered();
+}
diff --git a/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java b/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java
new file mode 100644
index 000000000..45395a71f
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.hint;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProvider;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.util.AccessibilityUtil;
+import java.util.Calendar;
+
+/**
+ * Selects a AnswerHint to show. If there's no suitable hints {@link EmptyAnswerHint} will be used,
+ * which does nothing.
+ */
+public class AnswerHintFactory {
+
+ private static final String CONFIG_ANSWER_HINT_ANSWERED_THRESHOLD_KEY =
+ "answer_hint_answered_threshold";
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final String CONFIG_ANSWER_HINT_WHITELISTED_DEVICES_KEY =
+ "answer_hint_whitelisted_devices";
+ // Most popular devices released before NDR1 is whitelisted. Their user are likely to have seen
+ // the legacy UI.
+ private static final String DEFAULT_WHITELISTED_DEVICES_CSV =
+ "/hammerhead//bullhead//angler//shamu//gm4g//gm4g_s//AQ4501//gce_x86_phone//gm4gtkc_s/"
+ + "/Sparkle_V//Mi-498//AQ4502//imobileiq2//A65//H940//m8_google//m0xx//A10//ctih220/"
+ + "/Mi438S//bacon/";
+
+ @VisibleForTesting
+ static final String ANSWERED_COUNT_PREFERENCE_KEY = "answer_hint_answered_count";
+
+ private final EventPayloadLoader eventPayloadLoader;
+
+ public AnswerHintFactory(@NonNull EventPayloadLoader eventPayloadLoader) {
+ this.eventPayloadLoader = Assert.isNotNull(eventPayloadLoader);
+ }
+
+ @NonNull
+ public AnswerHint create(Context context, long puckUpDuration, long puckUpDelay) {
+
+ if (shouldShowAnswerHint(
+ context,
+ ConfigProviderBindings.get(context),
+ getDeviceProtectedPreferences(context),
+ Build.PRODUCT)) {
+ return new DotAnswerHint(context, puckUpDuration, puckUpDelay);
+ }
+
+ // Display the event answer hint if the payload is available.
+ Drawable eventPayload =
+ eventPayloadLoader.loadPayload(
+ context, System.currentTimeMillis(), Calendar.getInstance().getTimeZone());
+ if (eventPayload != null) {
+ return new EventAnswerHint(context, eventPayload, puckUpDuration, puckUpDelay);
+ }
+
+ return new EmptyAnswerHint();
+ }
+
+ public static void increaseAnsweredCount(Context context) {
+ SharedPreferences sharedPreferences = getDeviceProtectedPreferences(context);
+ int answeredCount = sharedPreferences.getInt(ANSWERED_COUNT_PREFERENCE_KEY, 0);
+ sharedPreferences.edit().putInt(ANSWERED_COUNT_PREFERENCE_KEY, answeredCount + 1).apply();
+ }
+
+ @VisibleForTesting
+ static boolean shouldShowAnswerHint(
+ Context context,
+ ConfigProvider configProvider,
+ SharedPreferences sharedPreferences,
+ String device) {
+ if (AccessibilityUtil.isTouchExplorationEnabled(context)) {
+ return false;
+ }
+ // Devices that has the legacy dialer installed are whitelisted as they are likely to go through
+ // a UX change during updates.
+ if (!isDeviceWhitelisted(device, configProvider)) {
+ return false;
+ }
+
+ // If the user has gone through the process a few times we can assume they have learnt the
+ // method.
+ int answeredCount = sharedPreferences.getInt(ANSWERED_COUNT_PREFERENCE_KEY, 0);
+ long threshold = configProvider.getLong(CONFIG_ANSWER_HINT_ANSWERED_THRESHOLD_KEY, 3);
+ LogUtil.i(
+ "AnswerHintFactory.shouldShowAnswerHint",
+ "answerCount: %d, threshold: %d",
+ answeredCount,
+ threshold);
+ return answeredCount < threshold;
+ }
+
+ /**
+ * @param device should be the value of{@link Build#PRODUCT}.
+ * @param configProvider should provide a list of devices quoted with '/' concatenated to a
+ * string.
+ */
+ private static boolean isDeviceWhitelisted(String device, ConfigProvider configProvider) {
+ return configProvider
+ .getString(CONFIG_ANSWER_HINT_WHITELISTED_DEVICES_KEY, DEFAULT_WHITELISTED_DEVICES_CSV)
+ .contains("/" + device + "/");
+ }
+
+ private static SharedPreferences getDeviceProtectedPreferences(Context context) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ return PreferenceManager.getDefaultSharedPreferences(context);
+ }
+ return PreferenceManager.getDefaultSharedPreferences(
+ context.createDeviceProtectedStorageContext());
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/hint/DotAnswerHint.java b/java/com/android/incallui/answer/impl/hint/DotAnswerHint.java
new file mode 100644
index 000000000..394fe5808
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/DotAnswerHint.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.hint;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.support.annotation.DimenRes;
+import android.support.v4.view.animation.FastOutSlowInInterpolator;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.widget.TextView;
+
+/** An Answer hint that uses a green swiping dot. */
+public class DotAnswerHint implements AnswerHint {
+
+ private static final float ANSWER_HINT_SMALL_ALPHA = 0.8f;
+ private static final float ANSWER_HINT_MID_ALPHA = 0.5f;
+ private static final float ANSWER_HINT_LARGE_ALPHA = 0.2f;
+
+ private static final long FADE_IN_DELAY_SCALE_MILLIS = 380;
+ private static final long FADE_IN_DURATION_SCALE_MILLIS = 200;
+ private static final long FADE_IN_DELAY_ALPHA_MILLIS = 340;
+ private static final long FADE_IN_DURATION_ALPHA_MILLIS = 50;
+
+ private static final long SWIPE_UP_DURATION_ALPHA_MILLIS = 500;
+
+ private static final long FADE_OUT_DELAY_SCALE_SMALL_MILLIS = 90;
+ private static final long FADE_OUT_DELAY_SCALE_MID_MILLIS = 70;
+ private static final long FADE_OUT_DELAY_SCALE_LARGE_MILLIS = 10;
+ private static final long FADE_OUT_DURATION_SCALE_MILLIS = 100;
+ private static final long FADE_OUT_DELAY_ALPHA_MILLIS = 130;
+ private static final long FADE_OUT_DURATION_ALPHA_MILLIS = 170;
+
+ private final Context context;
+ private final long puckUpDurationMillis;
+ private final long puckUpDelayMillis;
+
+ private View puck;
+
+ private View answerHintSmall;
+ private View answerHintMid;
+ private View answerHintLarge;
+ private View answerHintContainer;
+ private AnimatorSet answerGestureHintAnim;
+
+ public DotAnswerHint(Context context, long puckUpDurationMillis, long puckUpDelayMillis) {
+ this.context = context;
+ this.puckUpDurationMillis = puckUpDurationMillis;
+ this.puckUpDelayMillis = puckUpDelayMillis;
+ }
+
+ @Override
+ public void onCreateView(
+ LayoutInflater inflater, ViewGroup container, View puck, TextView hintText) {
+ this.puck = puck;
+ View view = inflater.inflate(R.layout.dot_hint, container, true);
+ answerHintContainer = view.findViewById(R.id.answer_hint_container);
+ answerHintSmall = view.findViewById(R.id.answer_hint_small);
+ answerHintMid = view.findViewById(R.id.answer_hint_mid);
+ answerHintLarge = view.findViewById(R.id.answer_hint_large);
+ hintText.setTextSize(
+ TypedValue.COMPLEX_UNIT_PX, context.getResources().getDimension(R.dimen.hint_text_size));
+ }
+
+ @Override
+ public void onBounceStart() {
+ if (answerGestureHintAnim == null) {
+ answerGestureHintAnim = new AnimatorSet();
+ answerHintContainer.setY(puck.getY() + getDimension(R.dimen.hint_initial_offset));
+
+ Animator fadeIn = createFadeIn();
+
+ Animator swipeUp =
+ ObjectAnimator.ofFloat(
+ answerHintContainer,
+ View.TRANSLATION_Y,
+ puck.getY() - getDimension(R.dimen.hint_offset));
+ swipeUp.setInterpolator(new FastOutSlowInInterpolator());
+ swipeUp.setDuration(SWIPE_UP_DURATION_ALPHA_MILLIS);
+
+ Animator fadeOut = createFadeOut();
+
+ answerGestureHintAnim.play(fadeIn).after(puckUpDelayMillis);
+ answerGestureHintAnim.play(swipeUp).after(fadeIn);
+ // The fade out should start fading the alpha just as the puck is dropping. Scaling will start
+ // a bit earlier.
+ answerGestureHintAnim
+ .play(fadeOut)
+ .after(puckUpDelayMillis + puckUpDurationMillis - FADE_OUT_DELAY_ALPHA_MILLIS);
+
+ fadeIn.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ answerHintSmall.setAlpha(0);
+ answerHintSmall.setScaleX(1);
+ answerHintSmall.setScaleY(1);
+ answerHintMid.setAlpha(0);
+ answerHintMid.setScaleX(1);
+ answerHintMid.setScaleY(1);
+ answerHintLarge.setAlpha(0);
+ answerHintLarge.setScaleX(1);
+ answerHintLarge.setScaleY(1);
+ answerHintContainer.setY(puck.getY() + getDimension(R.dimen.hint_initial_offset));
+ answerHintContainer.setVisibility(View.VISIBLE);
+ }
+ });
+ }
+
+ answerGestureHintAnim.start();
+ }
+
+ private Animator createFadeIn() {
+ AnimatorSet set = new AnimatorSet();
+ set.play(
+ createFadeInScaleAndAlpha(
+ answerHintSmall,
+ R.dimen.hint_small_begin_size,
+ R.dimen.hint_small_end_size,
+ ANSWER_HINT_SMALL_ALPHA))
+ .with(
+ createFadeInScaleAndAlpha(
+ answerHintMid,
+ R.dimen.hint_mid_begin_size,
+ R.dimen.hint_mid_end_size,
+ ANSWER_HINT_MID_ALPHA))
+ .with(
+ createFadeInScaleAndAlpha(
+ answerHintLarge,
+ R.dimen.hint_large_begin_size,
+ R.dimen.hint_large_end_size,
+ ANSWER_HINT_LARGE_ALPHA));
+ return set;
+ }
+
+ private Animator createFadeInScaleAndAlpha(
+ View target, @DimenRes int beginSize, @DimenRes int endSize, float endAlpha) {
+ Animator scale =
+ createUniformScaleAnimator(
+ target,
+ getDimension(beginSize),
+ getDimension(beginSize),
+ getDimension(endSize),
+ FADE_IN_DURATION_SCALE_MILLIS,
+ FADE_IN_DELAY_SCALE_MILLIS,
+ new LinearInterpolator());
+ Animator alpha =
+ createAlphaAnimator(
+ target,
+ 0f,
+ endAlpha,
+ FADE_IN_DURATION_ALPHA_MILLIS,
+ FADE_IN_DELAY_ALPHA_MILLIS,
+ new LinearInterpolator());
+ AnimatorSet set = new AnimatorSet();
+ set.play(scale).with(alpha);
+ return set;
+ }
+
+ private Animator createFadeOut() {
+ AnimatorSet set = new AnimatorSet();
+ set.play(
+ createFadeOutScaleAndAlpha(
+ answerHintSmall,
+ R.dimen.hint_small_begin_size,
+ R.dimen.hint_small_end_size,
+ FADE_OUT_DELAY_SCALE_SMALL_MILLIS,
+ ANSWER_HINT_SMALL_ALPHA))
+ .with(
+ createFadeOutScaleAndAlpha(
+ answerHintMid,
+ R.dimen.hint_mid_begin_size,
+ R.dimen.hint_mid_end_size,
+ FADE_OUT_DELAY_SCALE_MID_MILLIS,
+ ANSWER_HINT_MID_ALPHA))
+ .with(
+ createFadeOutScaleAndAlpha(
+ answerHintLarge,
+ R.dimen.hint_large_begin_size,
+ R.dimen.hint_large_end_size,
+ FADE_OUT_DELAY_SCALE_LARGE_MILLIS,
+ ANSWER_HINT_LARGE_ALPHA));
+ return set;
+ }
+
+ private Animator createFadeOutScaleAndAlpha(
+ View target,
+ @DimenRes int beginSize,
+ @DimenRes int endSize,
+ long scaleDelay,
+ float endAlpha) {
+ Animator scale =
+ createUniformScaleAnimator(
+ target,
+ getDimension(beginSize),
+ getDimension(endSize),
+ getDimension(beginSize),
+ FADE_OUT_DURATION_SCALE_MILLIS,
+ scaleDelay,
+ new LinearInterpolator());
+ Animator alpha =
+ createAlphaAnimator(
+ target,
+ endAlpha,
+ 0.0f,
+ FADE_OUT_DURATION_ALPHA_MILLIS,
+ FADE_OUT_DELAY_ALPHA_MILLIS,
+ new LinearInterpolator());
+ AnimatorSet set = new AnimatorSet();
+ set.play(scale).with(alpha);
+ return set;
+ }
+
+ @Override
+ public void onBounceEnd() {
+ if (answerGestureHintAnim != null) {
+ answerGestureHintAnim.end();
+ answerGestureHintAnim = null;
+ answerHintContainer.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void onAnswered() {
+ AnswerHintFactory.increaseAnsweredCount(context);
+ }
+
+ private float getDimension(@DimenRes int id) {
+ return context.getResources().getDimension(id);
+ }
+
+ private static Animator createUniformScaleAnimator(
+ View target,
+ float original,
+ float begin,
+ float end,
+ long duration,
+ long delay,
+ Interpolator interpolator) {
+ float scaleBegin = begin / original;
+ float scaleEnd = end / original;
+ Animator scaleX = ObjectAnimator.ofFloat(target, View.SCALE_X, scaleBegin, scaleEnd);
+ Animator scaleY = ObjectAnimator.ofFloat(target, View.SCALE_Y, scaleBegin, scaleEnd);
+ scaleX.setDuration(duration);
+ scaleY.setDuration(duration);
+ scaleX.setInterpolator(interpolator);
+ scaleY.setInterpolator(interpolator);
+ AnimatorSet set = new AnimatorSet();
+ set.play(scaleX).with(scaleY).after(delay);
+ return set;
+ }
+
+ private static Animator createAlphaAnimator(
+ View target, float begin, float end, long duration, long delay, Interpolator interpolator) {
+ Animator alpha = ObjectAnimator.ofFloat(target, View.ALPHA, begin, end);
+ alpha.setDuration(duration);
+ alpha.setInterpolator(interpolator);
+ alpha.setStartDelay(delay);
+ return alpha;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/hint/EmptyAnswerHint.java b/java/com/android/incallui/answer/impl/hint/EmptyAnswerHint.java
new file mode 100644
index 000000000..e52b4ee36
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/EmptyAnswerHint.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.hint;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+/** Does nothing. Used to avoid null checks on AnswerHint. */
+public class EmptyAnswerHint implements AnswerHint {
+
+ @Override
+ public void onCreateView(
+ LayoutInflater inflater, ViewGroup container, View puck, TextView hintText) {}
+
+ @Override
+ public void onBounceStart() {}
+
+ @Override
+ public void onBounceEnd() {}
+
+ @Override
+ public void onAnswered() {}
+}
diff --git a/java/com/android/incallui/answer/impl/hint/EventAnswerHint.java b/java/com/android/incallui/answer/impl/hint/EventAnswerHint.java
new file mode 100644
index 000000000..7ee327d50
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/EventAnswerHint.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.hint;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.DimenRes;
+import android.support.annotation.NonNull;
+import android.support.v4.view.animation.FastOutSlowInInterpolator;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.android.dialer.common.Assert;
+
+/**
+ * An Answer hint that animates a {@link Drawable} payload with animation similar to {@link
+ * DotAnswerHint}.
+ */
+public final class EventAnswerHint implements AnswerHint {
+
+ private static final long FADE_IN_DELAY_SCALE_MILLIS = 380;
+ private static final long FADE_IN_DURATION_SCALE_MILLIS = 200;
+ private static final long FADE_IN_DELAY_ALPHA_MILLIS = 340;
+ private static final long FADE_IN_DURATION_ALPHA_MILLIS = 50;
+
+ private static final long SWIPE_UP_DURATION_ALPHA_MILLIS = 500;
+
+ private static final long FADE_OUT_DELAY_SCALE_SMALL_MILLIS = 90;
+ private static final long FADE_OUT_DURATION_SCALE_MILLIS = 100;
+ private static final long FADE_OUT_DELAY_ALPHA_MILLIS = 130;
+ private static final long FADE_OUT_DURATION_ALPHA_MILLIS = 170;
+
+ private static final float FADE_SCALE = 1.2f;
+
+ private final Context context;
+ private final Drawable payload;
+ private final long puckUpDurationMillis;
+ private final long puckUpDelayMillis;
+
+ private View puck;
+ private View payloadView;
+ private View answerHintContainer;
+ private AnimatorSet answerGestureHintAnim;
+
+ public EventAnswerHint(
+ @NonNull Context context,
+ @NonNull Drawable payload,
+ long puckUpDurationMillis,
+ long puckUpDelayMillis) {
+ this.context = Assert.isNotNull(context);
+ this.payload = Assert.isNotNull(payload);
+ this.puckUpDurationMillis = puckUpDurationMillis;
+ this.puckUpDelayMillis = puckUpDelayMillis;
+ }
+
+ @Override
+ public void onCreateView(
+ LayoutInflater inflater, ViewGroup container, View puck, TextView hintText) {
+ this.puck = puck;
+ View view = inflater.inflate(R.layout.event_hint, container, true);
+ answerHintContainer = view.findViewById(R.id.answer_hint_container);
+ payloadView = view.findViewById(R.id.payload);
+ hintText.setTextSize(
+ TypedValue.COMPLEX_UNIT_PX, context.getResources().getDimension(R.dimen.hint_text_size));
+ ((ImageView) payloadView).setImageDrawable(payload);
+ }
+
+ @Override
+ public void onBounceStart() {
+ if (answerGestureHintAnim == null) {
+
+ answerGestureHintAnim = new AnimatorSet();
+ answerHintContainer.setY(puck.getY() + getDimension(R.dimen.hint_initial_offset));
+
+ Animator fadeIn = createFadeIn();
+
+ Animator swipeUp =
+ ObjectAnimator.ofFloat(
+ answerHintContainer,
+ View.TRANSLATION_Y,
+ puck.getY() - getDimension(R.dimen.hint_offset));
+ swipeUp.setInterpolator(new FastOutSlowInInterpolator());
+ swipeUp.setDuration(SWIPE_UP_DURATION_ALPHA_MILLIS);
+
+ Animator fadeOut = createFadeOut();
+
+ answerGestureHintAnim.play(fadeIn).after(puckUpDelayMillis);
+ answerGestureHintAnim.play(swipeUp).after(fadeIn);
+ // The fade out should start fading the alpha just as the puck is dropping. Scaling will start
+ // a bit earlier.
+ answerGestureHintAnim
+ .play(fadeOut)
+ .after(puckUpDelayMillis + puckUpDurationMillis - FADE_OUT_DELAY_ALPHA_MILLIS);
+
+ fadeIn.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ payloadView.setAlpha(0);
+ payloadView.setScaleX(1);
+ payloadView.setScaleY(1);
+ answerHintContainer.setY(puck.getY() + getDimension(R.dimen.hint_initial_offset));
+ answerHintContainer.setVisibility(View.VISIBLE);
+ }
+ });
+ }
+
+ answerGestureHintAnim.start();
+ }
+
+ private Animator createFadeIn() {
+ AnimatorSet set = new AnimatorSet();
+ set.play(createFadeInScaleAndAlpha(payloadView));
+ return set;
+ }
+
+ private static Animator createFadeInScaleAndAlpha(View target) {
+ Animator scale =
+ createUniformScaleAnimator(
+ target,
+ FADE_SCALE,
+ 1.0f,
+ FADE_IN_DURATION_SCALE_MILLIS,
+ FADE_IN_DELAY_SCALE_MILLIS,
+ new LinearInterpolator());
+ Animator alpha =
+ createAlphaAnimator(
+ target,
+ 0f,
+ 1.0f,
+ FADE_IN_DURATION_ALPHA_MILLIS,
+ FADE_IN_DELAY_ALPHA_MILLIS,
+ new LinearInterpolator());
+ AnimatorSet set = new AnimatorSet();
+ set.play(scale).with(alpha);
+ return set;
+ }
+
+ private Animator createFadeOut() {
+ AnimatorSet set = new AnimatorSet();
+ set.play(createFadeOutScaleAndAlpha(payloadView, FADE_OUT_DELAY_SCALE_SMALL_MILLIS));
+ return set;
+ }
+
+ private static Animator createFadeOutScaleAndAlpha(View target, long scaleDelay) {
+ Animator scale =
+ createUniformScaleAnimator(
+ target,
+ 1.0f,
+ FADE_SCALE,
+ FADE_OUT_DURATION_SCALE_MILLIS,
+ scaleDelay,
+ new LinearInterpolator());
+ Animator alpha =
+ createAlphaAnimator(
+ target,
+ 01.0f,
+ 0.0f,
+ FADE_OUT_DURATION_ALPHA_MILLIS,
+ FADE_OUT_DELAY_ALPHA_MILLIS,
+ new LinearInterpolator());
+ AnimatorSet set = new AnimatorSet();
+ set.play(scale).with(alpha);
+ return set;
+ }
+
+ @Override
+ public void onBounceEnd() {
+ if (answerGestureHintAnim != null) {
+ answerGestureHintAnim.end();
+ answerGestureHintAnim = null;
+ answerHintContainer.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void onAnswered() {
+ // Do nothing
+ }
+
+ private float getDimension(@DimenRes int id) {
+ return context.getResources().getDimension(id);
+ }
+
+ private static Animator createUniformScaleAnimator(
+ View target,
+ float scaleBegin,
+ float scaleEnd,
+ long duration,
+ long delay,
+ Interpolator interpolator) {
+ Animator scaleX = ObjectAnimator.ofFloat(target, View.SCALE_X, scaleBegin, scaleEnd);
+ Animator scaleY = ObjectAnimator.ofFloat(target, View.SCALE_Y, scaleBegin, scaleEnd);
+ scaleX.setDuration(duration);
+ scaleY.setDuration(duration);
+ scaleX.setInterpolator(interpolator);
+ scaleY.setInterpolator(interpolator);
+ AnimatorSet set = new AnimatorSet();
+ set.play(scaleX).with(scaleY).after(delay);
+ return set;
+ }
+
+ private static Animator createAlphaAnimator(
+ View target, float begin, float end, long duration, long delay, Interpolator interpolator) {
+ Animator alpha = ObjectAnimator.ofFloat(target, View.ALPHA, begin, end);
+ alpha.setDuration(duration);
+ alpha.setInterpolator(interpolator);
+ alpha.setStartDelay(delay);
+ return alpha;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java b/java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java
new file mode 100644
index 000000000..09e3bedf2
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.hint;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import java.util.TimeZone;
+
+/** Loads a {@link Drawable} payload for the {@link EventAnswerHint} if it should be displayed. */
+public interface EventPayloadLoader {
+ @Nullable
+ Drawable loadPayload(
+ @NonNull Context context, long currentTimeUtcMillis, @NonNull TimeZone timeZone);
+}
diff --git a/java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java b/java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java
new file mode 100644
index 000000000..bd8d73645
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.hint;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Build.VERSION_CODES;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProvider;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import java.io.InputStream;
+import java.util.TimeZone;
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+
+/** Decrypt the event payload to be shown if in a specific time range and the key is received. */
+@TargetApi(VERSION_CODES.M)
+public final class EventPayloadLoaderImpl implements EventPayloadLoader {
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final String CONFIG_EVENT_KEY = "event_key";
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final String CONFIG_EVENT_BINARY = "event_binary";
+
+ // Time is stored as a UTC UNIX timestamp in milliseconds, but interpreted as local time.
+ // For example, 946684800 (2000/1/1 00:00:00 @UTC) is the new year midnight at every timezone.
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final String CONFIG_EVENT_START_UTC_AS_LOCAL_MILLIS = "event_time_start_millis";
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final String CONFIG_EVENT_TIME_END_UTC_AS_LOCAL_MILLIS = "event_time_end_millis";
+
+ @Override
+ @Nullable
+ public Drawable loadPayload(
+ @NonNull Context context, long currentTimeUtcMillis, @NonNull TimeZone timeZone) {
+ Assert.isNotNull(context);
+ Assert.isNotNull(timeZone);
+ ConfigProvider configProvider = ConfigProviderBindings.get(context);
+
+ String pbeKey = configProvider.getString(CONFIG_EVENT_KEY, null);
+ if (pbeKey == null) {
+ return null;
+ }
+ long timeRangeStart = configProvider.getLong(CONFIG_EVENT_START_UTC_AS_LOCAL_MILLIS, 0);
+ long timeRangeEnd = configProvider.getLong(CONFIG_EVENT_TIME_END_UTC_AS_LOCAL_MILLIS, 0);
+
+ String eventBinary = configProvider.getString(CONFIG_EVENT_BINARY, null);
+ if (eventBinary == null) {
+ return null;
+ }
+
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ if (!preferences.getBoolean(
+ EventSecretCodeListener.EVENT_ENABLED_WITH_SECRET_CODE_KEY, false)) {
+ long localTimestamp = currentTimeUtcMillis + timeZone.getRawOffset();
+
+ if (localTimestamp < timeRangeStart) {
+ return null;
+ }
+
+ if (localTimestamp > timeRangeEnd) {
+ return null;
+ }
+ }
+
+ // Use openssl aes-128-cbc -in <input> -out <output> -pass <PBEKey> to generate the asset
+ try (InputStream input = context.getAssets().open(eventBinary)) {
+ byte[] encryptedFile = new byte[input.available()];
+ input.read(encryptedFile);
+
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding", "BC");
+
+ byte[] salt = new byte[8];
+ System.arraycopy(encryptedFile, 8, salt, 0, 8);
+ SecretKey key =
+ SecretKeyFactory.getInstance("PBEWITHMD5AND128BITAES-CBC-OPENSSL", "BC")
+ .generateSecret(new PBEKeySpec(pbeKey.toCharArray(), salt, 100));
+ cipher.init(Cipher.DECRYPT_MODE, key);
+
+ byte[] decryptedFile = cipher.doFinal(encryptedFile, 16, encryptedFile.length - 16);
+
+ return new BitmapDrawable(
+ context.getResources(),
+ BitmapFactory.decodeByteArray(decryptedFile, 0, decryptedFile.length));
+ } catch (Exception e) {
+ // Avoid crashing dialer for any reason.
+ LogUtil.e("EventPayloadLoader.loadPayload", "error decrypting payload:", e);
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java b/java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java
new file mode 100644
index 000000000..7cf4054a9
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.hint;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.widget.Toast;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+
+/**
+ * Listen to the broadcast when the user dials "*#*#[number]#*#*" to toggle the event answer hint.
+ */
+public class EventSecretCodeListener extends BroadcastReceiver {
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final String CONFIG_EVENT_SECRET_CODE = "event_secret_code";
+
+ public static final String EVENT_ENABLED_WITH_SECRET_CODE_KEY = "event_enabled_with_secret_code";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String host = intent.getData().getHost();
+ String secretCode =
+ ConfigProviderBindings.get(context).getString(CONFIG_EVENT_SECRET_CODE, null);
+ if (secretCode == null) {
+ return;
+ }
+ if (!TextUtils.equals(secretCode, host)) {
+ return;
+ }
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean wasEnabled = preferences.getBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, false);
+ if (wasEnabled) {
+ preferences.edit().putBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, false).apply();
+ Toast.makeText(context, R.string.event_deactivated, Toast.LENGTH_SHORT).show();
+ Logger.get(context).logImpression(DialerImpression.Type.EVENT_ANSWER_HINT_DEACTIVATED);
+ LogUtil.i("EventSecretCodeListener.onReceive", "EventAnswerHint disabled");
+ } else {
+ preferences.edit().putBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, true).apply();
+ Toast.makeText(context, R.string.event_activated, Toast.LENGTH_SHORT).show();
+ Logger.get(context).logImpression(DialerImpression.Type.EVENT_ANSWER_HINT_ACTIVATED);
+ LogUtil.i("EventSecretCodeListener.onReceive", "EventAnswerHint enabled");
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_large.xml b/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_large.xml
new file mode 100644
index 000000000..f585ce5c9
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_large.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
+ <solid android:color="#00C853"/>
+</shape> \ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_mid.xml b/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_mid.xml
new file mode 100644
index 000000000..f585ce5c9
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_mid.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
+ <solid android:color="#00C853"/>
+</shape> \ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_small.xml b/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_small.xml
new file mode 100644
index 000000000..6a24d6a5f
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_small.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
+ <solid android:color="#00C853"/>
+ <stroke android:color="#00C853" android:width="2dp"/>
+ </shape> \ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/hint/res/layout/dot_hint.xml b/java/com/android/incallui/answer/impl/hint/res/layout/dot_hint.xml
new file mode 100644
index 000000000..84b10e736
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/layout/dot_hint.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/answer_hint_container"
+ android:layout_width="160dp"
+ android:layout_height="160dp"
+ android:layout_gravity="center_horizontal"
+ android:visibility="gone">
+ <ImageView
+ android:id="@+id/answer_hint_large"
+ android:layout_width="@dimen/hint_large_begin_size"
+ android:layout_height="@dimen/hint_large_begin_size"
+ android:layout_gravity="center"
+ android:alpha="0"
+ android:src="@drawable/answer_hint_large"/>
+ <ImageView
+ android:id="@+id/answer_hint_mid"
+ android:layout_width="@dimen/hint_mid_begin_size"
+ android:layout_height="@dimen/hint_mid_begin_size"
+ android:src="@drawable/answer_hint_mid"
+ android:alpha="0"
+ android:layout_gravity="center"/>
+ <ImageView
+ android:id="@+id/answer_hint_small"
+ android:layout_width="@dimen/hint_small_begin_size"
+ android:layout_height="@dimen/hint_small_begin_size"
+ android:src="@drawable/answer_hint_small"
+ android:alpha="0"
+ android:layout_gravity="center" />
+</FrameLayout> \ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml b/java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml
new file mode 100644
index 000000000..d505014c1
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/answer_hint_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center_horizontal"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:visibility="gone">
+ <ImageView
+ android:id="@+id/payload"
+ android:layout_width="191dp"
+ android:layout_height="773dp"
+ android:layout_gravity="center"
+ android:alpha="0"
+ android:rotation="-30"
+ android:transformPivotY="90dp"
+ android:clipChildren="false"
+ android:clipToPadding="false"/>
+</FrameLayout> \ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/hint/res/values/dimens.xml b/java/com/android/incallui/answer/impl/hint/res/values/dimens.xml
new file mode 100644
index 000000000..d86084b74
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/values/dimens.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="hint_text_size">18sp</dimen>
+ <dimen name="hint_initial_offset">-100dp</dimen>
+ <dimen name="hint_offset">300dp</dimen>
+ <dimen name="hint_small_begin_size">50dp</dimen>
+ <dimen name="hint_small_end_size">42dp</dimen>
+ <dimen name="hint_mid_begin_size">56dp</dimen>
+ <dimen name="hint_mid_end_size">64dp</dimen>
+ <dimen name="hint_large_begin_size">64dp</dimen>
+ <dimen name="hint_large_end_size">160dp</dimen>
+</resources> \ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/hint/res/values/strings.xml b/java/com/android/incallui/answer/impl/hint/res/values/strings.xml
new file mode 100644
index 000000000..d76021ae1
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/values/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="event_activated">Event Activated</string>
+ <string name="event_deactivated">Event Deactvated</string>
+</resources> \ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/res/anim/incoming_unlocked_icon_entry.xml b/java/com/android/incallui/answer/impl/res/anim/incoming_unlocked_icon_entry.xml
new file mode 100644
index 000000000..6490bbc5b
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/anim/incoming_unlocked_icon_entry.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:ordering="together">
+ <alpha
+ android:duration="583"
+ android:fromAlpha="0.0"
+ android:interpolator="@android:anim/accelerate_interpolator"
+ android:startOffset="167"
+ android:toAlpha="1.0"/>
+ <scale
+ android:duration="600"
+ android:fromXScale="0px"
+ android:fromYScale="0px"
+ android:interpolator="@android:anim/accelerate_interpolator"
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:toXScale="100%"
+ android:toYScale="100%"/>
+</set>
diff --git a/java/com/android/incallui/answer/impl/res/anim/incoming_unlocked_text_entry.xml b/java/com/android/incallui/answer/impl/res/anim/incoming_unlocked_text_entry.xml
new file mode 100644
index 000000000..9d3195a79
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/anim/incoming_unlocked_text_entry.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <alpha
+ android:duration="583"
+ android:fromAlpha="0.0"
+ android:interpolator="@android:anim/accelerate_interpolator"
+ android:startOffset="167"
+ android:toAlpha="1.0"/>
+</set>
diff --git a/java/com/android/incallui/answer/impl/res/layout/fragment_avatar.xml b/java/com/android/incallui/answer/impl/res/layout/fragment_avatar.xml
new file mode 100644
index 000000000..d656ceb4e
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/layout/fragment_avatar.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+
+<ImageView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@id/contactgrid_avatar"
+ android:layout_width="@dimen/answer_avatar_size"
+ android:layout_height="@dimen/answer_avatar_size"
+ android:layout_marginTop="20dp"
+ android:layout_gravity="center_horizontal"
+ android:elevation="@dimen/answer_data_elevation"/>
diff --git a/java/com/android/incallui/answer/impl/res/layout/fragment_custom_sms_dialog.xml b/java/com/android/incallui/answer/impl/res/layout/fragment_custom_sms_dialog.xml
new file mode 100644
index 000000000..c36386ead
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/layout/fragment_custom_sms_dialog.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingLeft="24dp"
+ android:paddingRight="24dp">
+
+ <EditText
+ android:id="@+id/custom_sms_input"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+</FrameLayout> \ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/res/layout/fragment_incoming_call.xml b/java/com/android/incallui/answer/impl/res/layout/fragment_incoming_call.xml
new file mode 100644
index 000000000..aa153dd4b
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/layout/fragment_incoming_call.xml
@@ -0,0 +1,152 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<com.android.incallui.answer.impl.AffordanceHolderLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/incoming_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:keepScreenOn="true">
+
+ <TextureView
+ android:id="@+id/incoming_preview_texture_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:importantForAccessibility="no"
+ android:visibility="gone"/>
+
+ <View
+ android:id="@+id/incoming_preview_texture_view_overlay"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/videocall_overlay_background_color"
+ android:visibility="gone"/>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
+
+ <TextView
+ android:id="@+id/videocall_video_off"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:padding="64dp"
+ android:accessibilityTraversalBefore="@+id/videocall_speaker_button"
+ android:drawablePadding="8dp"
+ android:drawableTop="@drawable/quantum_ic_videocam_off_white_36"
+ android:gravity="center"
+ android:text="@string/call_incoming_video_is_off"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance"
+ android:visibility="gone"/>
+
+ <LinearLayout
+ android:id="@+id/incall_contact_grid"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="24dp"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:gravity="top|center_horizontal"
+ android:orientation="vertical">
+
+ <include
+ android:id="@id/contactgrid_top_row"
+ layout="@layout/incall_contactgrid_top_row"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:layout_marginStart="24dp"
+ android:layout_marginEnd="24dp"/>
+
+ <!-- We have to keep deprecated singleLine to allow long text being truncated with ellipses.
+ b/31396406 -->
+ <com.android.incallui.autoresizetext.AutoResizeTextView
+ android:id="@id/contactgrid_contact_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:layout_marginStart="24dp"
+ android:layout_marginEnd="24dp"
+ android:singleLine="true"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance.Large"
+ android:textSize="@dimen/answer_contact_name_text_size"
+ app:autoResizeText_minTextSize="@dimen/answer_contact_name_min_size"
+ tools:ignore="Deprecated"
+ tools:text="Jake Peralta"/>
+
+ <include
+ android:id="@id/contactgrid_bottom_row"
+ layout="@layout/incall_contactgrid_bottom_row"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="24dp"
+ android:layout_marginEnd="24dp"/>
+
+ <TextView
+ android:id="@+id/incall_important_call_badge"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="@dimen/answer_importance_margin_bottom"
+ android:elevation="@dimen/answer_data_elevation"
+ android:gravity="center"
+ android:singleLine="true"
+ android:text="@string/call_incoming_important"
+ android:textAllCaps="true"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance"
+ android:textColor="@android:color/black"/>
+
+ <FrameLayout
+ android:id="@+id/incall_location_holder"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <FrameLayout
+ android:id="@+id/incall_data_container"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/answer_data_size"
+ android:clipChildren="false"
+ android:clipToPadding="false"/>
+
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/answer_method_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false"
+ android:clipToPadding="false"/>
+
+ </FrameLayout>
+
+ <com.android.incallui.answer.impl.affordance.SwipeButtonView
+ android:id="@+id/incoming_secondary_button"
+ android:layout_width="56dp"
+ android:layout_height="56dp"
+ android:layout_gravity="bottom|start"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_message_white_24"
+ android:visibility="invisible"
+ tools:visibility="visible"/>
+
+</com.android.incallui.answer.impl.AffordanceHolderLayout>
diff --git a/java/com/android/incallui/answer/impl/res/values-h240dp/dimens.xml b/java/com/android/incallui/answer/impl/res/values-h240dp/dimens.xml
new file mode 100644
index 000000000..ca384ef8d
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/values-h240dp/dimens.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <dimen name="answer_contact_name_text_size">36sp</dimen>
+ <dimen name="answer_contact_name_min_size">32sp</dimen>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/res/values-h300dp/dimens.xml b/java/com/android/incallui/answer/impl/res/values-h300dp/dimens.xml
new file mode 100644
index 000000000..fdecbb7bf
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/values-h300dp/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <dimen name="answer_contact_name_text_size">54sp</dimen>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/res/values-h480dp/dimens.xml b/java/com/android/incallui/answer/impl/res/values-h480dp/dimens.xml
new file mode 100644
index 000000000..5dc3f2ac5
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/values-h480dp/dimens.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <dimen name="answer_data_size">150dp</dimen>
+ <dimen name="answer_avatar_size">100dp</dimen>
+ <dimen name="answer_importance_margin_bottom">8dp</dimen>
+ <bool name="answer_important_call_allowed">true</bool>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/res/values-h540dp/dimens.xml b/java/com/android/incallui/answer/impl/res/values-h540dp/dimens.xml
new file mode 100644
index 000000000..69716e0bd
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/values-h540dp/dimens.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <dimen name="answer_data_size">258dp</dimen>
+ <dimen name="answer_avatar_size">172dp</dimen>
+ <dimen name="answer_importance_margin_bottom">8dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/res/values/dimens.xml b/java/com/android/incallui/answer/impl/res/values/dimens.xml
new file mode 100644
index 000000000..c48b68f93
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/values/dimens.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <dimen name="answer_contact_name_text_size">24sp</dimen>
+ <dimen name="answer_contact_name_min_size">24sp</dimen>
+ <dimen name="answer_data_size">0dp</dimen>
+ <dimen name="answer_avatar_size">0dp</dimen>
+ <dimen name="answer_importance_margin_bottom">0dp</dimen>
+ <bool name="answer_important_call_allowed">false</bool>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/res/values/strings.xml b/java/com/android/incallui/answer/impl/res/values/strings.xml
new file mode 100644
index 000000000..7fc91fce4
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/values/strings.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="call_incoming_swipe_to_decline_with_message">Swipe from icon to decline with message</string>
+ <string name="call_incoming_swipe_to_answer_video_as_audio">Swipe from icon to answer as an audio call</string>
+ <string name="call_incoming_message_custom">Write your own…</string>
+ <string name="call_incoming_audio_handset">Handset</string>
+ <string name="call_incoming_audio_speakerphone">Speakerphone</string>
+ <!-- "Respond via SMS" option that lets you compose a custom response. [CHAR LIMIT=30] -->
+ <string name="call_incoming_respond_via_sms_custom_message">Write your own…</string>
+ <!-- "Custom Message" Cancel alert dialog button -->
+ <string name="call_incoming_custom_message_cancel">Cancel</string>
+ <!-- "Custom Message" Send alert dialog button -->
+ <string name="call_incoming_custom_message_send">Send</string>
+ <string name="a11y_incoming_call_reject_with_sms">Reject this call with a message</string>
+ <string name="a11y_incoming_call_answer_video_as_audio">Answer as audio call</string>
+ <string name="a11y_description_incoming_call_reject_with_sms">Reject with message</string>
+ <string name="a11y_description_incoming_call_answer_video_as_audio">Answer as audio call</string>
+
+ <!-- Text indicates the video local camera is off. [CHAR LIMIT=40] -->
+ <string name="call_incoming_video_is_off">Video is off</string>
+
+ <!-- Voice prompt of swipe gesture when accessibility is turned on. -->
+ <string description="The message announced to accessibility assistance on incoming call."
+ name="a11y_incoming_call_swipe_gesture_prompt">Two finger swipe up to answer. Two finger swipe down to decline.</string>
+ <string name="call_incoming_important">Important call</string>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/utils/FlingAnimationUtils.java b/java/com/android/incallui/answer/impl/utils/FlingAnimationUtils.java
new file mode 100644
index 000000000..3acb2a205
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/utils/FlingAnimationUtils.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.utils;
+
+import android.animation.Animator;
+import android.content.Context;
+import android.view.ViewPropertyAnimator;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+/** Utility class to calculate general fling animation when the finger is released. */
+public class FlingAnimationUtils {
+
+ private static final float LINEAR_OUT_SLOW_IN_X2 = 0.35f;
+ private static final float LINEAR_OUT_FASTER_IN_X2 = 0.5f;
+ private static final float LINEAR_OUT_FASTER_IN_Y2_MIN = 0.4f;
+ private static final float LINEAR_OUT_FASTER_IN_Y2_MAX = 0.5f;
+ private static final float MIN_VELOCITY_DP_PER_SECOND = 250;
+ private static final float HIGH_VELOCITY_DP_PER_SECOND = 3000;
+
+ /** Crazy math. http://en.wikipedia.org/wiki/B%C3%A9zier_curve */
+ private static final float LINEAR_OUT_SLOW_IN_START_GRADIENT = 1.0f / LINEAR_OUT_SLOW_IN_X2;
+
+ private Interpolator linearOutSlowIn;
+
+ private float minVelocityPxPerSecond;
+ private float maxLengthSeconds;
+ private float highVelocityPxPerSecond;
+
+ private AnimatorProperties mAnimatorProperties = new AnimatorProperties();
+
+ public FlingAnimationUtils(Context ctx, float maxLengthSeconds) {
+ this.maxLengthSeconds = maxLengthSeconds;
+ linearOutSlowIn = new PathInterpolator(0, 0, LINEAR_OUT_SLOW_IN_X2, 1);
+ minVelocityPxPerSecond =
+ MIN_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density;
+ highVelocityPxPerSecond =
+ HIGH_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density;
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ */
+ public void apply(Animator animator, float currValue, float endValue, float velocity) {
+ apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue));
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ */
+ public void apply(
+ ViewPropertyAnimator animator, float currValue, float endValue, float velocity) {
+ apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue));
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ * @param maxDistance the maximum distance for this interaction; the maximum animation length gets
+ * multiplied by the ratio between the actual distance and this value
+ */
+ public void apply(
+ Animator animator, float currValue, float endValue, float velocity, float maxDistance) {
+ AnimatorProperties properties = getProperties(currValue, endValue, velocity, maxDistance);
+ animator.setDuration(properties.duration);
+ animator.setInterpolator(properties.interpolator);
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ * @param maxDistance the maximum distance for this interaction; the maximum animation length gets
+ * multiplied by the ratio between the actual distance and this value
+ */
+ public void apply(
+ ViewPropertyAnimator animator,
+ float currValue,
+ float endValue,
+ float velocity,
+ float maxDistance) {
+ AnimatorProperties properties = getProperties(currValue, endValue, velocity, maxDistance);
+ animator.setDuration(properties.duration);
+ animator.setInterpolator(properties.interpolator);
+ }
+
+ private AnimatorProperties getProperties(
+ float currValue, float endValue, float velocity, float maxDistance) {
+ float maxLengthSeconds =
+ (float) (this.maxLengthSeconds * Math.sqrt(Math.abs(endValue - currValue) / maxDistance));
+ float diff = Math.abs(endValue - currValue);
+ float velAbs = Math.abs(velocity);
+ float durationSeconds = LINEAR_OUT_SLOW_IN_START_GRADIENT * diff / velAbs;
+ if (durationSeconds <= maxLengthSeconds) {
+ mAnimatorProperties.interpolator = linearOutSlowIn;
+ } else if (velAbs >= minVelocityPxPerSecond) {
+
+ // Cross fade between fast-out-slow-in and linear interpolator with current velocity.
+ durationSeconds = maxLengthSeconds;
+ VelocityInterpolator velocityInterpolator =
+ new VelocityInterpolator(durationSeconds, velAbs, diff);
+ mAnimatorProperties.interpolator =
+ new InterpolatorInterpolator(velocityInterpolator, linearOutSlowIn, linearOutSlowIn);
+ } else {
+
+ // Just use a normal interpolator which doesn't take the velocity into account.
+ durationSeconds = maxLengthSeconds;
+ mAnimatorProperties.interpolator = Interpolators.FAST_OUT_SLOW_IN;
+ }
+ mAnimatorProperties.duration = (long) (durationSeconds * 1000);
+ return mAnimatorProperties;
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion for the case when the animation is making something
+ * disappear.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ * @param maxDistance the maximum distance for this interaction; the maximum animation length gets
+ * multiplied by the ratio between the actual distance and this value
+ */
+ public void applyDismissing(
+ Animator animator, float currValue, float endValue, float velocity, float maxDistance) {
+ AnimatorProperties properties =
+ getDismissingProperties(currValue, endValue, velocity, maxDistance);
+ animator.setDuration(properties.duration);
+ animator.setInterpolator(properties.interpolator);
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion for the case when the animation is making something
+ * disappear.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ * @param maxDistance the maximum distance for this interaction; the maximum animation length gets
+ * multiplied by the ratio between the actual distance and this value
+ */
+ public void applyDismissing(
+ ViewPropertyAnimator animator,
+ float currValue,
+ float endValue,
+ float velocity,
+ float maxDistance) {
+ AnimatorProperties properties =
+ getDismissingProperties(currValue, endValue, velocity, maxDistance);
+ animator.setDuration(properties.duration);
+ animator.setInterpolator(properties.interpolator);
+ }
+
+ private AnimatorProperties getDismissingProperties(
+ float currValue, float endValue, float velocity, float maxDistance) {
+ float maxLengthSeconds =
+ (float)
+ (this.maxLengthSeconds * Math.pow(Math.abs(endValue - currValue) / maxDistance, 0.5f));
+ float diff = Math.abs(endValue - currValue);
+ float velAbs = Math.abs(velocity);
+ float y2 = calculateLinearOutFasterInY2(velAbs);
+
+ float startGradient = y2 / LINEAR_OUT_FASTER_IN_X2;
+ Interpolator mLinearOutFasterIn = new PathInterpolator(0, 0, LINEAR_OUT_FASTER_IN_X2, y2);
+ float durationSeconds = startGradient * diff / velAbs;
+ if (durationSeconds <= maxLengthSeconds) {
+ mAnimatorProperties.interpolator = mLinearOutFasterIn;
+ } else if (velAbs >= minVelocityPxPerSecond) {
+
+ // Cross fade between linear-out-faster-in and linear interpolator with current
+ // velocity.
+ durationSeconds = maxLengthSeconds;
+ VelocityInterpolator velocityInterpolator =
+ new VelocityInterpolator(durationSeconds, velAbs, diff);
+ InterpolatorInterpolator superInterpolator =
+ new InterpolatorInterpolator(velocityInterpolator, mLinearOutFasterIn, linearOutSlowIn);
+ mAnimatorProperties.interpolator = superInterpolator;
+ } else {
+
+ // Just use a normal interpolator which doesn't take the velocity into account.
+ durationSeconds = maxLengthSeconds;
+ mAnimatorProperties.interpolator = Interpolators.FAST_OUT_LINEAR_IN;
+ }
+ mAnimatorProperties.duration = (long) (durationSeconds * 1000);
+ return mAnimatorProperties;
+ }
+
+ /**
+ * Calculates the y2 control point for a linear-out-faster-in path interpolator depending on the
+ * velocity. The faster the velocity, the more "linear" the interpolator gets.
+ *
+ * @param velocity the velocity of the gesture.
+ * @return the y2 control point for a cubic bezier path interpolator
+ */
+ private float calculateLinearOutFasterInY2(float velocity) {
+ float t =
+ (velocity - minVelocityPxPerSecond) / (highVelocityPxPerSecond - minVelocityPxPerSecond);
+ t = Math.max(0, Math.min(1, t));
+ return (1 - t) * LINEAR_OUT_FASTER_IN_Y2_MIN + t * LINEAR_OUT_FASTER_IN_Y2_MAX;
+ }
+
+ /** @return the minimum velocity a gesture needs to have to be considered a fling */
+ public float getMinVelocityPxPerSecond() {
+ return minVelocityPxPerSecond;
+ }
+
+ /** An interpolator which interpolates two interpolators with an interpolator. */
+ private static final class InterpolatorInterpolator implements Interpolator {
+
+ private Interpolator mInterpolator1;
+ private Interpolator mInterpolator2;
+ private Interpolator mCrossfader;
+
+ InterpolatorInterpolator(
+ Interpolator interpolator1, Interpolator interpolator2, Interpolator crossfader) {
+ mInterpolator1 = interpolator1;
+ mInterpolator2 = interpolator2;
+ mCrossfader = crossfader;
+ }
+
+ @Override
+ public float getInterpolation(float input) {
+ float t = mCrossfader.getInterpolation(input);
+ return (1 - t) * mInterpolator1.getInterpolation(input)
+ + t * mInterpolator2.getInterpolation(input);
+ }
+ }
+
+ /** An interpolator which interpolates with a fixed velocity. */
+ private static final class VelocityInterpolator implements Interpolator {
+
+ private float mDurationSeconds;
+ private float mVelocity;
+ private float mDiff;
+
+ private VelocityInterpolator(float durationSeconds, float velocity, float diff) {
+ mDurationSeconds = durationSeconds;
+ mVelocity = velocity;
+ mDiff = diff;
+ }
+
+ @Override
+ public float getInterpolation(float input) {
+ float time = input * mDurationSeconds;
+ return time * mVelocity / mDiff;
+ }
+ }
+
+ private static class AnimatorProperties {
+
+ Interpolator interpolator;
+ long duration;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/utils/Interpolators.java b/java/com/android/incallui/answer/impl/utils/Interpolators.java
new file mode 100644
index 000000000..efc68f78a
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/utils/Interpolators.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.utils;
+
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+/**
+ * Common interpolators used in answer methods.
+ */
+public class Interpolators {
+
+ public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
+ public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
+ public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
+}
diff --git a/java/com/android/incallui/answer/protocol/AnswerScreen.java b/java/com/android/incallui/answer/protocol/AnswerScreen.java
new file mode 100644
index 000000000..0c374eb7f
--- /dev/null
+++ b/java/com/android/incallui/answer/protocol/AnswerScreen.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.protocol;
+
+import android.support.v4.app.Fragment;
+import java.util.List;
+
+/** Interface for the answer module. */
+public interface AnswerScreen {
+
+ String getCallId();
+
+ int getVideoState();
+
+ boolean isVideoUpgradeRequest();
+
+ void setTextResponses(List<String> textResponses);
+
+ boolean hasPendingDialogs();
+
+ void dismissPendingDialogs();
+
+ Fragment getAnswerScreenFragment();
+}
diff --git a/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java b/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java
new file mode 100644
index 000000000..9934497cf
--- /dev/null
+++ b/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.protocol;
+
+import android.support.annotation.FloatRange;
+
+/** Callbacks implemented by the container app for this module. */
+public interface AnswerScreenDelegate {
+
+ void onAnswerScreenUnready();
+
+ void onDismissDialog();
+
+ void onRejectCallWithMessage(String message);
+
+ void onAnswer(int videoState);
+
+ void onReject();
+
+ /**
+ * Sets the window background color based on foreground call's theme and the given progress. This
+ * is called from the answer UI to animate the accept and reject action.
+ *
+ * <p>When the user is rejecting we animate the background color to a mostly transparent gray. The
+ * end effect is that the home screen shows through.
+ *
+ * @param progress float from -1 to 1. -1 is fully rejected, 1 is fully accepted, and 0 is neutral
+ */
+ void updateWindowBackgroundColor(@FloatRange(from = -1f, to = 1.0f) float progress);
+}
diff --git a/java/com/android/incallui/answer/protocol/AnswerScreenDelegateFactory.java b/java/com/android/incallui/answer/protocol/AnswerScreenDelegateFactory.java
new file mode 100644
index 000000000..a09cb1a40
--- /dev/null
+++ b/java/com/android/incallui/answer/protocol/AnswerScreenDelegateFactory.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.protocol;
+
+/** Used to create an instance of the delegate, should be implemented by the container activity. */
+public interface AnswerScreenDelegateFactory {
+
+ AnswerScreenDelegate newAnswerScreenDelegate(AnswerScreen answerScreen);
+}
diff --git a/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java b/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
new file mode 100644
index 000000000..edc3db34b
--- /dev/null
+++ b/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answerproximitysensor;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.os.PowerManager;
+import android.view.Display;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.call.DialerCall.State;
+import com.android.incallui.call.DialerCallListener;
+
+/**
+ * This class prevents users from accidentally answering calls by keeping the screen off until the
+ * proximity sensor is unblocked. If the screen is already on or if this is a call waiting call then
+ * nothing is done.
+ */
+public class AnswerProximitySensor
+ implements DialerCallListener, AnswerProximityWakeLock.ScreenOnListener {
+
+ private static final String CONFIG_ANSWER_PROXIMITY_SENSOR_ENABLED =
+ "answer_proximity_sensor_enabled";
+ private static final String CONFIG_ANSWER_PSEUDO_PROXIMITY_WAKE_LOCK_ENABLED =
+ "answer_pseudo_proximity_wake_lock_enabled";
+
+ private final DialerCall call;
+ private final AnswerProximityWakeLock answerProximityWakeLock;
+
+ public static boolean shouldUse(Context context, DialerCall call) {
+ // Don't use the AnswerProximitySensor for call waiting and other states. Those states are
+ // handled by the general ProximitySensor code.
+ if (call.getState() != State.INCOMING) {
+ LogUtil.i("AnswerProximitySensor.shouldUse", "call state is not incoming");
+ return false;
+ }
+
+ if (!ConfigProviderBindings.get(context)
+ .getBoolean(CONFIG_ANSWER_PROXIMITY_SENSOR_ENABLED, true)) {
+ LogUtil.i("AnswerProximitySensor.shouldUse", "disabled by config");
+ return false;
+ }
+
+ if (!context
+ .getSystemService(PowerManager.class)
+ .isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
+ LogUtil.i("AnswerProximitySensor.shouldUse", "wake lock level not supported");
+ return false;
+ }
+
+ if (isDefaultDisplayOn(context)) {
+ LogUtil.i("AnswerProximitySensor.shouldUse", "display is already on");
+ return false;
+ }
+
+ return true;
+ }
+
+ public AnswerProximitySensor(
+ Context context, DialerCall call, PseudoScreenState pseudoScreenState) {
+ this.call = call;
+
+ LogUtil.i("AnswerProximitySensor.constructor", "acquiring lock");
+ if (ConfigProviderBindings.get(context)
+ .getBoolean(CONFIG_ANSWER_PSEUDO_PROXIMITY_WAKE_LOCK_ENABLED, true)) {
+ answerProximityWakeLock = new PseudoProximityWakeLock(context, pseudoScreenState);
+ } else {
+ // TODO: choose a wake lock implementation base on framework/device.
+ // These bugs requires the PseudoProximityWakeLock workaround:
+ // b/30439151 Proximity sensor not working on M
+ // b/31499931 fautly touch input when screen is off on marlin/sailfish
+ answerProximityWakeLock = new SystemProximityWakeLock(context);
+ }
+ answerProximityWakeLock.setScreenOnListener(this);
+ answerProximityWakeLock.acquire();
+
+ call.addListener(this);
+ }
+
+ private void cleanup() {
+ call.removeListener(this);
+ releaseProximityWakeLock();
+ }
+
+ private void releaseProximityWakeLock() {
+ if (answerProximityWakeLock.isHeld()) {
+ LogUtil.i("AnswerProximitySensor.releaseProximityWakeLock", "releasing lock");
+ answerProximityWakeLock.release();
+ }
+ }
+
+ private static boolean isDefaultDisplayOn(Context context) {
+ Display display =
+ context.getSystemService(DisplayManager.class).getDisplay(Display.DEFAULT_DISPLAY);
+ return display.getState() == Display.STATE_ON;
+ }
+
+ @Override
+ public void onDialerCallDisconnect() {
+ LogUtil.i("AnswerProximitySensor.onDialerCallDisconnect", null);
+ cleanup();
+ }
+
+ @Override
+ public void onDialerCallUpdate() {
+ if (call.getState() != State.INCOMING) {
+ LogUtil.i("AnswerProximitySensor.onDialerCallUpdate", "no longer incoming, cleaning up");
+ cleanup();
+ }
+ }
+
+ @Override
+ public void onDialerCallChildNumberChange() {}
+
+ @Override
+ public void onDialerCallLastForwardedNumberChange() {}
+
+ @Override
+ public void onDialerCallUpgradeToVideo() {}
+
+ @Override
+ public void onWiFiToLteHandover() {}
+
+ @Override
+ public void onHandoverToWifiFailure() {}
+
+ @Override
+ public void onDialerCallSessionModificationStateChange(@SessionModificationState int state) {}
+
+ @Override
+ public void onScreenOn() {
+ cleanup();
+ }
+}
diff --git a/java/com/android/incallui/answerproximitysensor/AnswerProximityWakeLock.java b/java/com/android/incallui/answerproximitysensor/AnswerProximityWakeLock.java
new file mode 100644
index 000000000..94abe9c85
--- /dev/null
+++ b/java/com/android/incallui/answerproximitysensor/AnswerProximityWakeLock.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.answerproximitysensor;
+
+/**
+ * Interface to wrap around the {@link android.os.PowerManager.WakeLock} for custom implementations.
+ */
+public interface AnswerProximityWakeLock {
+
+ /** Called when the wake lock turned the screen back on. */
+ interface ScreenOnListener {
+
+ void onScreenOn();
+ }
+
+ void acquire();
+
+ void release();
+
+ boolean isHeld();
+
+ void setScreenOnListener(ScreenOnListener listener);
+}
diff --git a/java/com/android/incallui/answerproximitysensor/PseudoProximityWakeLock.java b/java/com/android/incallui/answerproximitysensor/PseudoProximityWakeLock.java
new file mode 100644
index 000000000..c7844d47d
--- /dev/null
+++ b/java/com/android/incallui/answerproximitysensor/PseudoProximityWakeLock.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.answerproximitysensor;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.LogUtil;
+
+/**
+ * A fake PROXIMITY_SCREEN_OFF_WAKE_LOCK implemented by the app. It will use {@link
+ * PseudoScreenState} to fake a black screen when the proximity sensor is near.
+ */
+public class PseudoProximityWakeLock implements AnswerProximityWakeLock, SensorEventListener {
+
+ private final Context context;
+ private final PseudoScreenState pseudoScreenState;
+ private final Sensor proximitySensor;
+
+ @Nullable private ScreenOnListener listener;
+ private boolean isHeld;
+
+ public PseudoProximityWakeLock(Context context, PseudoScreenState pseudoScreenState) {
+ this.context = context;
+ this.pseudoScreenState = pseudoScreenState;
+ pseudoScreenState.setOn(true);
+ proximitySensor =
+ context.getSystemService(SensorManager.class).getDefaultSensor(Sensor.TYPE_PROXIMITY);
+ }
+
+ @Override
+ public void acquire() {
+ isHeld = true;
+ context
+ .getSystemService(SensorManager.class)
+ .registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
+ }
+
+ @Override
+ public void release() {
+ isHeld = false;
+ context.getSystemService(SensorManager.class).unregisterListener(this);
+ pseudoScreenState.setOn(true);
+ }
+
+ @Override
+ public boolean isHeld() {
+ return isHeld;
+ }
+
+ @Override
+ public void setScreenOnListener(ScreenOnListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent sensorEvent) {
+ boolean near = sensorEvent.values[0] < sensorEvent.sensor.getMaximumRange();
+ LogUtil.i("AnswerProximitySensor.PseudoProximityWakeLock.onSensorChanged", "near: " + near);
+ pseudoScreenState.setOn(!near);
+ if (!near && listener != null) {
+ listener.onScreenOn();
+ }
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int i) {}
+}
diff --git a/java/com/android/incallui/answerproximitysensor/PseudoScreenState.java b/java/com/android/incallui/answerproximitysensor/PseudoScreenState.java
new file mode 100644
index 000000000..eda0ee720
--- /dev/null
+++ b/java/com/android/incallui/answerproximitysensor/PseudoScreenState.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.answerproximitysensor;
+
+import android.util.ArraySet;
+import java.util.Set;
+
+/**
+ * Stores a fake screen on/off state for the {@link InCallActivity}. If InCallActivity see the state
+ * is off, it will draw a black view over the activity pretending the screen is off.
+ *
+ * <p>If the screen is already touched when the screen is turned on, the OS behavior is sending a
+ * new DOWN event once the point started moving and then behave as a normal gesture. To prevent
+ * accidental answer/rejects, touches that started when the screen is off should be ignored.
+ *
+ * <p>b/31499931 on certain devices with N-DR1, if the screen is already touched when the screen is
+ * turned on, a "DOWN MOVE UP" will be sent for each movement before the touch is actually released.
+ * These events is hard to discern from other normal events, and keeping the screen on reduces its'
+ * probability.
+ */
+public class PseudoScreenState {
+
+ /** Notifies when the on state has changed. */
+ public interface StateChangedListener {
+ void onPseudoScreenStateChanged(boolean isOn);
+ }
+
+ private final Set<StateChangedListener> listeners = new ArraySet<>();
+
+ private boolean on = true;
+
+ public boolean isOn() {
+ return on;
+ }
+
+ public void setOn(boolean value) {
+ if (on != value) {
+ on = value;
+ for (StateChangedListener listener : listeners) {
+ listener.onPseudoScreenStateChanged(on);
+ }
+ }
+ }
+
+ public void addListener(StateChangedListener listener) {
+ listeners.add(listener);
+ }
+
+ public void removeListener(StateChangedListener listener) {
+ listeners.remove(listener);
+ }
+}
diff --git a/java/com/android/incallui/answerproximitysensor/SystemProximityWakeLock.java b/java/com/android/incallui/answerproximitysensor/SystemProximityWakeLock.java
new file mode 100644
index 000000000..776e9a42d
--- /dev/null
+++ b/java/com/android/incallui/answerproximitysensor/SystemProximityWakeLock.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.answerproximitysensor;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.DisplayListener;
+import android.os.PowerManager;
+import android.support.annotation.Nullable;
+import android.view.Display;
+import com.android.dialer.common.LogUtil;
+
+/** The normal PROXIMITY_SCREEN_OFF_WAKE_LOCK provided by the OS. */
+public class SystemProximityWakeLock implements AnswerProximityWakeLock, DisplayListener {
+
+ private static final String TAG = "SystemProximityWakeLock";
+
+ private final Context context;
+ private final PowerManager.WakeLock wakeLock;
+
+ @Nullable private ScreenOnListener listener;
+
+ public SystemProximityWakeLock(Context context) {
+ this.context = context;
+ wakeLock =
+ context
+ .getSystemService(PowerManager.class)
+ .newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
+ }
+
+ @Override
+ public void acquire() {
+ wakeLock.acquire();
+ context.getSystemService(DisplayManager.class).registerDisplayListener(this, null);
+ }
+
+ @Override
+ public void release() {
+ wakeLock.release();
+ context.getSystemService(DisplayManager.class).unregisterDisplayListener(this);
+ }
+
+ @Override
+ public boolean isHeld() {
+ return wakeLock.isHeld();
+ }
+
+ @Override
+ public void setScreenOnListener(ScreenOnListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public void onDisplayAdded(int displayId) {}
+
+ @Override
+ public void onDisplayRemoved(int displayId) {}
+
+ @Override
+ public void onDisplayChanged(int displayId) {
+ if (displayId == Display.DEFAULT_DISPLAY) {
+ if (isDefaultDisplayOn(context)) {
+ LogUtil.i("SystemProximityWakeLock.onDisplayChanged", "display turned on");
+ if (listener != null) {
+ listener.onScreenOn();
+ }
+ }
+ }
+ }
+
+ private static boolean isDefaultDisplayOn(Context context) {
+ Display display =
+ context.getSystemService(DisplayManager.class).getDisplay(Display.DEFAULT_DISPLAY);
+ return display.getState() != Display.STATE_OFF;
+ }
+}
diff --git a/java/com/android/incallui/async/PausableExecutor.java b/java/com/android/incallui/async/PausableExecutor.java
new file mode 100644
index 000000000..e10757e67
--- /dev/null
+++ b/java/com/android/incallui/async/PausableExecutor.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.async;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Executor that can be used to easily synchronize testing and production code. Production code
+ * should call {@link #milestone()} at points in the code where the state of the system is worthy of
+ * testing. In a test scenario, this method will pause execution until the test acknowledges the
+ * milestone through the use of {@link #ackMilestoneForTesting()}.
+ */
+public interface PausableExecutor extends Executor {
+
+ /**
+ * Method called from asynchronous production code to inform this executor that it has reached a
+ * point that puts the system into a state worth testing. TestableExecutors intended for use in a
+ * testing environment should cause the calling thread to block. In the production environment
+ * this should be a no-op.
+ */
+ void milestone();
+
+ /**
+ * Method called from the test code to inform this executor that the state of the production
+ * system at the current milestone has been sufficiently tested. Every milestone must be
+ * acknowledged.
+ */
+ void ackMilestoneForTesting();
+
+ /**
+ * Method called from the test code to inform this executor that the tests are finished with all
+ * milestones. Future calls to {@link #milestone()} or {@link #awaitMilestoneForTesting()} should
+ * return immediately.
+ */
+ void ackAllMilestonesForTesting();
+
+ /**
+ * Method called from the test code to block until a milestone has been reached in the production
+ * code.
+ */
+ void awaitMilestoneForTesting() throws InterruptedException;
+}
diff --git a/java/com/android/incallui/async/PausableExecutorImpl.java b/java/com/android/incallui/async/PausableExecutorImpl.java
new file mode 100644
index 000000000..687606129
--- /dev/null
+++ b/java/com/android/incallui/async/PausableExecutorImpl.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.async;
+
+import java.util.concurrent.Executors;
+
+/** {@link PausableExecutor} intended for use in production environments. */
+public class PausableExecutorImpl implements PausableExecutor {
+
+ @Override
+ public void milestone() {}
+
+ @Override
+ public void ackMilestoneForTesting() {}
+
+ @Override
+ public void ackAllMilestonesForTesting() {}
+
+ @Override
+ public void awaitMilestoneForTesting() {}
+
+ @Override
+ public void execute(Runnable command) {
+ Executors.newSingleThreadExecutor().execute(command);
+ }
+}
diff --git a/java/com/android/incallui/audioroute/AndroidManifest.xml b/java/com/android/incallui/audioroute/AndroidManifest.xml
new file mode 100644
index 000000000..36431f1ee
--- /dev/null
+++ b/java/com/android/incallui/audioroute/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.incallui.audioroute">
+</manifest>
diff --git a/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java b/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java
new file mode 100644
index 000000000..c757477f1
--- /dev/null
+++ b/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.audioroute;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff.Mode;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.design.widget.BottomSheetDialogFragment;
+import android.telecom.CallAudioState;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.TextView;
+import com.android.dialer.common.FragmentUtils;
+import com.android.dialer.common.LogUtil;
+
+/** Shows picker for audio routes */
+public class AudioRouteSelectorDialogFragment extends BottomSheetDialogFragment {
+
+ private static final String ARG_AUDIO_STATE = "audio_state";
+
+ /** Called when an audio route is picked */
+ public interface AudioRouteSelectorPresenter {
+ void onAudioRouteSelected(int audioRoute);
+ }
+
+ public static AudioRouteSelectorDialogFragment newInstance(CallAudioState audioState) {
+ AudioRouteSelectorDialogFragment fragment = new AudioRouteSelectorDialogFragment();
+ Bundle args = new Bundle();
+ args.putParcelable(ARG_AUDIO_STATE, audioState);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ FragmentUtils.checkParent(this, AudioRouteSelectorPresenter.class);
+ }
+
+ @Override
+ public Dialog onCreateDialog(final Bundle savedInstanceState) {
+ LogUtil.i("AudioRouteSelectorDialogFragment.onCreateDialog", null);
+ Dialog dialog = super.onCreateDialog(savedInstanceState);
+ dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+ return dialog;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+ View view = layoutInflater.inflate(R.layout.audioroute_selector, viewGroup, false);
+ CallAudioState audioState = getArguments().getParcelable(ARG_AUDIO_STATE);
+
+ initItem(
+ (TextView) view.findViewById(R.id.audioroute_bluetooth),
+ CallAudioState.ROUTE_BLUETOOTH,
+ audioState);
+ initItem(
+ (TextView) view.findViewById(R.id.audioroute_speaker),
+ CallAudioState.ROUTE_SPEAKER,
+ audioState);
+ initItem(
+ (TextView) view.findViewById(R.id.audioroute_headset),
+ CallAudioState.ROUTE_WIRED_HEADSET,
+ audioState);
+ initItem(
+ (TextView) view.findViewById(R.id.audioroute_earpiece),
+ CallAudioState.ROUTE_EARPIECE,
+ audioState);
+ return view;
+ }
+
+ private void initItem(TextView item, final int itemRoute, CallAudioState audioState) {
+ int selectedColor = getResources().getColor(R.color.dialer_theme_color);
+ if ((audioState.getSupportedRouteMask() & itemRoute) == 0) {
+ item.setVisibility(View.GONE);
+ } else if (audioState.getRoute() == itemRoute) {
+ item.setTextColor(selectedColor);
+ item.setCompoundDrawableTintList(ColorStateList.valueOf(selectedColor));
+ item.setCompoundDrawableTintMode(Mode.SRC_ATOP);
+ }
+ item.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dismiss();
+ FragmentUtils.getParentUnsafe(
+ AudioRouteSelectorDialogFragment.this, AudioRouteSelectorPresenter.class)
+ .onAudioRouteSelected(itemRoute);
+ }
+ });
+ }
+}
diff --git a/java/com/android/incallui/audioroute/res/drawable-hdpi/ic_phone_audio_grey600_24dp.png b/java/com/android/incallui/audioroute/res/drawable-hdpi/ic_phone_audio_grey600_24dp.png
new file mode 100644
index 000000000..4ea921a3e
--- /dev/null
+++ b/java/com/android/incallui/audioroute/res/drawable-hdpi/ic_phone_audio_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/audioroute/res/drawable-mdpi/ic_phone_audio_grey600_24dp.png b/java/com/android/incallui/audioroute/res/drawable-mdpi/ic_phone_audio_grey600_24dp.png
new file mode 100644
index 000000000..acef550ac
--- /dev/null
+++ b/java/com/android/incallui/audioroute/res/drawable-mdpi/ic_phone_audio_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/audioroute/res/drawable-xhdpi/ic_phone_audio_grey600_24dp.png b/java/com/android/incallui/audioroute/res/drawable-xhdpi/ic_phone_audio_grey600_24dp.png
new file mode 100644
index 000000000..a30aa5c0c
--- /dev/null
+++ b/java/com/android/incallui/audioroute/res/drawable-xhdpi/ic_phone_audio_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/audioroute/res/drawable-xxhdpi/ic_phone_audio_grey600_24dp.png b/java/com/android/incallui/audioroute/res/drawable-xxhdpi/ic_phone_audio_grey600_24dp.png
new file mode 100644
index 000000000..beb85a80a
--- /dev/null
+++ b/java/com/android/incallui/audioroute/res/drawable-xxhdpi/ic_phone_audio_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/audioroute/res/layout/audioroute_selector.xml b/java/com/android/incallui/audioroute/res/layout/audioroute_selector.xml
new file mode 100644
index 000000000..ef2220e8f
--- /dev/null
+++ b/java/com/android/incallui/audioroute/res/layout/audioroute_selector.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ tools:layout_gravity="bottom">
+ <TextView
+ android:id="@+id/audioroute_bluetooth"
+ style="@style/AudioRouteItem"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/quantum_ic_bluetooth_audio_grey600_24"
+ android:text="@string/audioroute_bluetooth"/>
+ <TextView
+ android:id="@+id/audioroute_speaker"
+ style="@style/AudioRouteItem"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/quantum_ic_volume_up_grey600_24"
+ android:text="@string/audioroute_speaker"/>
+ <TextView
+ android:id="@+id/audioroute_earpiece"
+ style="@style/AudioRouteItem"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/ic_phone_audio_grey600_24dp"
+ android:text="@string/audioroute_phone"/>
+ <TextView
+ android:id="@+id/audioroute_headset"
+ style="@style/AudioRouteItem"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/quantum_ic_headset_grey600_24"
+ android:text="@string/audioroute_headset"/>
+
+</LinearLayout>
diff --git a/java/com/android/incallui/audioroute/res/values/strings.xml b/java/com/android/incallui/audioroute/res/values/strings.xml
new file mode 100644
index 000000000..b16639354
--- /dev/null
+++ b/java/com/android/incallui/audioroute/res/values/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="audioroute_bluetooth">Bluetooth</string>
+ <string name="audioroute_speaker">Speaker</string>
+ <string name="audioroute_phone">Phone</string>
+ <string name="audioroute_headset">Wired headset</string>
+</resources>
diff --git a/java/com/android/incallui/audioroute/res/values/styles.xml b/java/com/android/incallui/audioroute/res/values/styles.xml
new file mode 100644
index 000000000..4484b7092
--- /dev/null
+++ b/java/com/android/incallui/audioroute/res/values/styles.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="AudioRouteItem">
+ <item name="android:padding">16dp</item>
+ <item name="android:background">?android:selectableItemBackground</item>
+ <item name="android:drawablePadding">24dp</item>
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:textAppearance">
+ @style/TextAppearance.AppCompat.Light.Widget.PopupMenu.Large
+ </item>
+ <item name="android:textColor">?android:textColorSecondary</item>
+ </style>
+</resources>
diff --git a/java/com/android/incallui/autoresizetext/AndroidManifest.xml b/java/com/android/incallui/autoresizetext/AndroidManifest.xml
new file mode 100644
index 000000000..53a8961e4
--- /dev/null
+++ b/java/com/android/incallui/autoresizetext/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.incallui.autoresizetext">
+
+ <uses-sdk
+ android:minSdkVersion="23"
+ android:targetSdkVersion="25"/>
+
+ <application />
+</manifest>
diff --git a/java/com/android/incallui/autoresizetext/AutoResizeTextView.java b/java/com/android/incallui/autoresizetext/AutoResizeTextView.java
new file mode 100644
index 000000000..eedcbe5bb
--- /dev/null
+++ b/java/com/android/incallui/autoresizetext/AutoResizeTextView.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.autoresizetext;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.RectF;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.text.Layout.Alignment;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.SparseIntArray;
+import android.util.TypedValue;
+import android.widget.TextView;
+import javax.annotation.Nullable;
+
+/**
+ * A TextView that automatically scales its text to completely fill its allotted width.
+ *
+ * <p>Note: In some edge cases, the binary search algorithm to find the best fit may slightly
+ * overshoot / undershoot its constraints. See b/26704434. No minimal repro case has been
+ * found yet. A known workaround is the solution provided on StackOverflow:
+ * http://stackoverflow.com/a/5535672
+ */
+public class AutoResizeTextView extends TextView {
+ private static final int NO_LINE_LIMIT = -1;
+ private static final float DEFAULT_MIN_TEXT_SIZE = 16.0f;
+ private static final int DEFAULT_RESIZE_STEP_UNIT = TypedValue.COMPLEX_UNIT_PX;
+
+ private final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+ private final RectF availableSpaceRect = new RectF();
+ private final SparseIntArray textSizesCache = new SparseIntArray();
+ private final TextPaint textPaint = new TextPaint();
+ private int resizeStepUnit = DEFAULT_RESIZE_STEP_UNIT;
+ private float minTextSize = DEFAULT_MIN_TEXT_SIZE;
+ private float maxTextSize;
+ private int maxWidth;
+ private int maxLines;
+ private float lineSpacingMultiplier = 1.0f;
+ private float lineSpacingExtra = 0.0f;
+
+ public AutoResizeTextView(Context context) {
+ super(context, null, 0);
+ initialize(context, null, 0, 0);
+ }
+
+ public AutoResizeTextView(Context context, AttributeSet attrs) {
+ super(context, attrs, 0);
+ initialize(context, attrs, 0, 0);
+ }
+
+ public AutoResizeTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initialize(context, attrs, defStyleAttr, 0);
+ }
+
+ public AutoResizeTextView(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initialize(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ private void initialize(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ TypedArray typedArray = context.getTheme().obtainStyledAttributes(
+ attrs, R.styleable.AutoResizeTextView, defStyleAttr, defStyleRes);
+ readAttrs(typedArray);
+ textPaint.set(getPaint());
+ }
+
+ /** Overridden because getMaxLines is only defined in JB+. */
+ @Override
+ public final int getMaxLines() {
+ if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
+ return super.getMaxLines();
+ } else {
+ return maxLines;
+ }
+ }
+
+ /** Overridden because getMaxLines is only defined in JB+. */
+ @Override
+ public final void setMaxLines(int maxLines) {
+ super.setMaxLines(maxLines);
+ this.maxLines = maxLines;
+ }
+
+ /** Overridden because getLineSpacingMultiplier is only defined in JB+. */
+ @Override
+ public final float getLineSpacingMultiplier() {
+ if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
+ return super.getLineSpacingMultiplier();
+ } else {
+ return lineSpacingMultiplier;
+ }
+ }
+
+ /** Overridden because getLineSpacingExtra is only defined in JB+. */
+ @Override
+ public final float getLineSpacingExtra() {
+ if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
+ return super.getLineSpacingExtra();
+ } else {
+ return lineSpacingExtra;
+ }
+ }
+
+ /**
+ * Overridden because getLineSpacingMultiplier and getLineSpacingExtra are only defined in JB+.
+ */
+ @Override
+ public final void setLineSpacing(float add, float mult) {
+ super.setLineSpacing(add, mult);
+ lineSpacingMultiplier = mult;
+ lineSpacingExtra = add;
+ }
+
+ /**
+ * Although this overrides the setTextSize method from the TextView base class, it changes the
+ * semantics a bit: Calling setTextSize now specifies the maximum text size to be used by this
+ * view. If the text can't fit with that text size, the text size will be scaled down, up to the
+ * minimum text size specified in {@link #setMinTextSize}.
+ *
+ * <p>Note that the final size unit will be truncated to the nearest integer value of the
+ * specified unit.
+ */
+ @Override
+ public final void setTextSize(int unit, float size) {
+ float maxTextSize = TypedValue.applyDimension(unit, size, displayMetrics);
+ if (this.maxTextSize != maxTextSize) {
+ this.maxTextSize = maxTextSize;
+ // TODO: It's not actually necessary to clear the whole cache here. To optimize cache
+ // deletion we'd have to delete all entries in the cache with a value equal or larger than
+ // MIN(old_max_size, new_max_size) when changing maxTextSize; and all entries with a value
+ // equal or smaller than MAX(old_min_size, new_min_size) when changing minTextSize.
+ textSizesCache.clear();
+ requestLayout();
+ }
+ }
+
+ /**
+ * Sets the lower text size limit and invalidate the view.
+ *
+ * <p>The parameters follow the same behavior as they do in {@link #setTextSize}.
+ *
+ * <p>Note that the final size unit will be truncated to the nearest integer value of the
+ * specified unit.
+ */
+ public final void setMinTextSize(int unit, float size) {
+ float minTextSize = TypedValue.applyDimension(unit, size, displayMetrics);
+ if (this.minTextSize != minTextSize) {
+ this.minTextSize = minTextSize;
+ textSizesCache.clear();
+ requestLayout();
+ }
+ }
+
+ /**
+ * Sets the unit to use as step units when computing the resized font size. This view's text
+ * contents will always be rendered as a whole integer value in the unit specified here. For
+ * example, if the unit is {@link TypedValue#COMPLEX_UNIT_SP}, then the text size may end up
+ * being 13sp or 14sp, but never 13.5sp.
+ *
+ * <p>By default, the AutoResizeTextView uses the unit {@link TypedValue#COMPLEX_UNIT_PX}.
+ *
+ * @param unit the unit type to use; must be a known unit type from {@link TypedValue}.
+ */
+ public final void setResizeStepUnit(int unit) {
+ if (resizeStepUnit != unit) {
+ resizeStepUnit = unit;
+ requestLayout();
+ }
+ }
+
+ private void readAttrs(TypedArray typedArray) {
+ resizeStepUnit = typedArray.getInt(
+ R.styleable.AutoResizeTextView_autoResizeText_resizeStepUnit, DEFAULT_RESIZE_STEP_UNIT);
+ minTextSize = (int) typedArray.getDimension(
+ R.styleable.AutoResizeTextView_autoResizeText_minTextSize, DEFAULT_MIN_TEXT_SIZE);
+ maxTextSize = (int) getTextSize();
+ }
+
+ private void adjustTextSize() {
+ int maxWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
+ int maxHeight = getMeasuredHeight() - getPaddingBottom() - getPaddingTop();
+
+ if (maxWidth <= 0 || maxHeight <= 0) {
+ return;
+ }
+
+ this.maxWidth = maxWidth;
+ availableSpaceRect.right = maxWidth;
+ availableSpaceRect.bottom = maxHeight;
+ int minSizeInStepSizeUnits = (int) Math.ceil(convertToResizeStepUnits(minTextSize));
+ int maxSizeInStepSizeUnits = (int) Math.floor(convertToResizeStepUnits(maxTextSize));
+ float textSize = computeTextSize(
+ minSizeInStepSizeUnits, maxSizeInStepSizeUnits, availableSpaceRect);
+ super.setTextSize(resizeStepUnit, textSize);
+ }
+
+ private boolean suggestedSizeFitsInSpace(float suggestedSizeInPx, RectF availableSpace) {
+ textPaint.setTextSize(suggestedSizeInPx);
+ String text = getText().toString();
+ int maxLines = getMaxLines();
+ if (maxLines == 1) {
+ // If single line, check the line's height and width.
+ return textPaint.getFontSpacing() <= availableSpace.bottom
+ && textPaint.measureText(text) <= availableSpace.right;
+ } else {
+ // If multiline, lay the text out, then check the number of lines, the layout's height,
+ // and each line's width.
+ StaticLayout layout = new StaticLayout(text,
+ textPaint,
+ maxWidth,
+ Alignment.ALIGN_NORMAL,
+ getLineSpacingMultiplier(),
+ getLineSpacingExtra(),
+ true);
+
+ // Return false if we need more than maxLines. The text is obviously too big in this case.
+ if (maxLines != NO_LINE_LIMIT && layout.getLineCount() > maxLines) {
+ return false;
+ }
+ // Return false if the height of the layout is too big.
+ return layout.getHeight() <= availableSpace.bottom;
+ }
+ }
+
+ /**
+ * Computes the final text size to use for this text view, factoring in any previously
+ * cached computations.
+ *
+ * @param minSize the minimum text size to allow, in units of {@link #resizeStepUnit}
+ * @param maxSize the maximum text size to allow, in units of {@link #resizeStepUnit}
+ */
+ private float computeTextSize(int minSize, int maxSize, RectF availableSpace) {
+ CharSequence text = getText();
+ if (text != null && textSizesCache.get(text.hashCode()) != 0) {
+ return textSizesCache.get(text.hashCode());
+ }
+ int size = binarySearchSizes(minSize, maxSize, availableSpace);
+ textSizesCache.put(text == null ? 0 : text.hashCode(), size);
+ return size;
+ }
+
+ /**
+ * Performs a binary search to find the largest font size that will still fit within the size
+ * available to this view.
+ * @param minSize the minimum text size to allow, in units of {@link #resizeStepUnit}
+ * @param maxSize the maximum text size to allow, in units of {@link #resizeStepUnit}
+ */
+ private int binarySearchSizes(int minSize, int maxSize, RectF availableSpace) {
+ int bestSize = minSize;
+ int low = minSize + 1;
+ int high = maxSize;
+ int sizeToTry;
+ while (low <= high) {
+ sizeToTry = (low + high) / 2;
+ float dimension = TypedValue.applyDimension(resizeStepUnit, sizeToTry, displayMetrics);
+ if (suggestedSizeFitsInSpace(dimension, availableSpace)) {
+ bestSize = low;
+ low = sizeToTry + 1;
+ } else {
+ high = sizeToTry - 1;
+ bestSize = high;
+ }
+ }
+ return bestSize;
+ }
+
+ private float convertToResizeStepUnits(float dimension) {
+ // To figure out the multiplier between a raw dimension and the resizeStepUnit, we invert the
+ // conversion of 1 resizeStepUnit to a raw dimension.
+ float multiplier = 1 / TypedValue.applyDimension(resizeStepUnit, 1, displayMetrics);
+ return dimension * multiplier;
+ }
+
+ @Override
+ protected final void onTextChanged(
+ final CharSequence text, final int start, final int before, final int after) {
+ super.onTextChanged(text, start, before, after);
+ adjustTextSize();
+ }
+
+ @Override
+ protected final void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
+ super.onSizeChanged(width, height, oldWidth, oldHeight);
+ if (width != oldWidth || height != oldHeight) {
+ textSizesCache.clear();
+ adjustTextSize();
+ }
+ }
+
+ @Override
+ protected final void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ adjustTextSize();
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+}
diff --git a/java/com/android/incallui/autoresizetext/res/values/attrs.xml b/java/com/android/incallui/autoresizetext/res/values/attrs.xml
new file mode 100644
index 000000000..e62feb9c8
--- /dev/null
+++ b/java/com/android/incallui/autoresizetext/res/values/attrs.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <declare-styleable name="AutoResizeTextView">
+ <!--
+ The unit to use when computing step increments for the resize operation. That is, the
+ resized text will be guaranteed to be a whole number (integer) value in the unit
+ specified. For example, if the unit is scaled pixels (sp), then the font size might be
+ 13sp or 14sp, but not 13.5sp.
+
+ The enum values must match the values from android.util.TypedValue.
+ -->
+ <attr name="autoResizeText_resizeStepUnit" format="enum">
+ <!-- Must match TypedValue.COMPLEX_UNIT_PX. -->
+ <enum name="unitPx" value="0" />
+ <!-- Must match TypedValue.COMPLEX_UNIT_DIP. -->
+ <enum name="unitDip" value="1" />
+ <!-- Must match TypedValue.COMPLEX_UNIT_SP. -->
+ <enum name="unitSp" value="2" />
+ <!-- Must match TypedValue.COMPLEX_UNIT_PT. -->
+ <enum name="unitPt" value="3" />
+ <!-- Must match TypedValue.COMPLEX_UNIT_IN. -->
+ <enum name="unitIn" value="4" />
+ <!-- Must match TypedValue.COMPLEX_UNIT_MM. -->
+ <enum name="unitMm" value="5" />
+ </attr>
+ <!--
+ The minimum text size to use in this view. Text size will be scale down to fit the text
+ in this view, but no smaller than the minimum size specified in this attribute.
+ -->
+ <attr name="autoResizeText_minTextSize" format="dimension" />
+ </declare-styleable>
+</resources>
diff --git a/java/com/android/incallui/baseui/BaseFragment.java b/java/com/android/incallui/baseui/BaseFragment.java
new file mode 100644
index 000000000..58b8c6f8d
--- /dev/null
+++ b/java/com/android/incallui/baseui/BaseFragment.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.baseui;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+
+/** Parent for all fragments that use Presenters and Ui design. */
+public abstract class BaseFragment<T extends Presenter<U>, U extends Ui> extends Fragment {
+
+ private static final String KEY_FRAGMENT_HIDDEN = "key_fragment_hidden";
+
+ private T mPresenter;
+
+ protected BaseFragment() {
+ mPresenter = createPresenter();
+ }
+
+ public abstract T createPresenter();
+
+ public abstract U getUi();
+
+ /**
+ * Presenter will be available after onActivityCreated().
+ *
+ * @return The presenter associated with this fragment.
+ */
+ public T getPresenter() {
+ return mPresenter;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ mPresenter.onUiReady(getUi());
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ mPresenter.onRestoreInstanceState(savedInstanceState);
+ if (savedInstanceState.getBoolean(KEY_FRAGMENT_HIDDEN)) {
+ getFragmentManager().beginTransaction().hide(this).commit();
+ }
+ }
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ mPresenter.onUiDestroy(getUi());
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mPresenter.onSaveInstanceState(outState);
+ outState.putBoolean(KEY_FRAGMENT_HIDDEN, isHidden());
+ }
+}
diff --git a/java/com/android/incallui/baseui/Presenter.java b/java/com/android/incallui/baseui/Presenter.java
new file mode 100644
index 000000000..581ad47c7
--- /dev/null
+++ b/java/com/android/incallui/baseui/Presenter.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.baseui;
+
+import android.os.Bundle;
+
+/** Base class for Presenters. */
+public abstract class Presenter<U extends Ui> {
+
+ private U mUi;
+
+ /**
+ * Called after the UI view has been created. That is when fragment.onViewCreated() is called.
+ *
+ * @param ui The Ui implementation that is now ready to be used.
+ */
+ public void onUiReady(U ui) {
+ mUi = ui;
+ }
+
+ /** Called when the UI view is destroyed in Fragment.onDestroyView(). */
+ public final void onUiDestroy(U ui) {
+ onUiUnready(ui);
+ mUi = null;
+ }
+
+ /**
+ * To be overriden by Presenter implementations. Called when the fragment is being destroyed but
+ * before ui is set to null.
+ */
+ public void onUiUnready(U ui) {}
+
+ public void onSaveInstanceState(Bundle outState) {}
+
+ public void onRestoreInstanceState(Bundle savedInstanceState) {}
+
+ public U getUi() {
+ return mUi;
+ }
+}
diff --git a/java/com/android/incallui/baseui/Ui.java b/java/com/android/incallui/baseui/Ui.java
new file mode 100644
index 000000000..439e41550
--- /dev/null
+++ b/java/com/android/incallui/baseui/Ui.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.baseui;
+
+/** Base class for all presenter ui. */
+public interface Ui {}
diff --git a/java/com/android/incallui/bindings/ContactUtils.java b/java/com/android/incallui/bindings/ContactUtils.java
new file mode 100644
index 000000000..d2d365d81
--- /dev/null
+++ b/java/com/android/incallui/bindings/ContactUtils.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.bindings;
+
+import android.location.Address;
+import android.util.Pair;
+import java.util.Calendar;
+import java.util.List;
+
+/** Utility functions to help manipulate contact data. */
+public interface ContactUtils {
+
+ boolean retrieveContactInteractionsFromLookupKey(String lookupKey, Listener listener);
+
+ interface Listener {
+
+ void onContactInteractionsFound(Address address, List<Pair<Calendar, Calendar>> openingHours);
+ }
+}
diff --git a/java/com/android/incallui/bindings/DistanceHelper.java b/java/com/android/incallui/bindings/DistanceHelper.java
new file mode 100644
index 000000000..6b2200dca
--- /dev/null
+++ b/java/com/android/incallui/bindings/DistanceHelper.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.bindings;
+
+import android.location.Address;
+
+/** Superclass for a helper class to get the current location and distance to other locations. */
+public interface DistanceHelper {
+
+ float DISTANCE_NOT_FOUND = -1;
+ float MILES_PER_METER = (float) 0.000621371192;
+ float KILOMETERS_PER_METER = (float) 0.001;
+
+ void cleanUp();
+
+ float calculateDistance(Address address);
+
+ interface Listener {
+
+ void onLocationReady();
+ }
+}
diff --git a/java/com/android/incallui/bindings/InCallUiBindings.java b/java/com/android/incallui/bindings/InCallUiBindings.java
new file mode 100644
index 000000000..d3d3a8b37
--- /dev/null
+++ b/java/com/android/incallui/bindings/InCallUiBindings.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.bindings;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.ConfigProvider;
+
+/** This interface allows the container application to customize the in call UI. */
+public interface InCallUiBindings {
+
+ @Nullable
+ PhoneNumberService newPhoneNumberService(Context context);
+
+ /** @return An {@link Intent} to be broadcast when the InCallUI is visible. */
+ @Nullable
+ Intent getUiReadyBroadcastIntent(Context context);
+
+ /**
+ * @return An {@link Intent} to be broadcast when the call state button in the InCallUI is touched
+ * while in a call.
+ */
+ @Nullable
+ Intent getCallStateButtonBroadcastIntent(Context context);
+
+ @Nullable
+ DistanceHelper newDistanceHelper(Context context, DistanceHelper.Listener listener);
+
+ @Nullable
+ ContactUtils getContactUtilsInstance(Context context);
+
+ ConfigProvider getConfigProvider();
+}
diff --git a/java/com/android/incallui/bindings/InCallUiBindingsFactory.java b/java/com/android/incallui/bindings/InCallUiBindingsFactory.java
new file mode 100644
index 000000000..57c186d90
--- /dev/null
+++ b/java/com/android/incallui/bindings/InCallUiBindingsFactory.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.bindings;
+
+/**
+ * This interface should be implementated by the Application subclass. It allows the in call UI
+ * module to get references to the InCallUiBindings.
+ */
+public interface InCallUiBindingsFactory {
+
+ InCallUiBindings newInCallUiBindings();
+}
diff --git a/java/com/android/incallui/bindings/InCallUiBindingsStub.java b/java/com/android/incallui/bindings/InCallUiBindingsStub.java
new file mode 100644
index 000000000..7b42fb375
--- /dev/null
+++ b/java/com/android/incallui/bindings/InCallUiBindingsStub.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.bindings;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.ConfigProvider;
+
+/** Default implementation for InCallUi bindings. */
+public class InCallUiBindingsStub implements InCallUiBindings {
+ private ConfigProvider configProvider;
+
+ @Override
+ @Nullable
+ public PhoneNumberService newPhoneNumberService(Context context) {
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public Intent getUiReadyBroadcastIntent(Context context) {
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public Intent getCallStateButtonBroadcastIntent(Context context) {
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public DistanceHelper newDistanceHelper(Context context, DistanceHelper.Listener listener) {
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public ContactUtils getContactUtilsInstance(Context context) {
+ return null;
+ }
+
+ @Override
+ public ConfigProvider getConfigProvider() {
+ if (configProvider == null) {
+ configProvider =
+ new ConfigProvider() {
+ @Override
+ public String getString(String key, String defaultValue) {
+ return defaultValue;
+ }
+
+ @Override
+ public long getLong(String key, long defaultValue) {
+ return defaultValue;
+ }
+
+ @Override
+ public boolean getBoolean(String key, boolean defaultValue) {
+ return defaultValue;
+ }
+ };
+ }
+ return configProvider;
+ }
+}
diff --git a/java/com/android/incallui/bindings/PhoneNumberService.java b/java/com/android/incallui/bindings/PhoneNumberService.java
new file mode 100644
index 000000000..bd2741a1d
--- /dev/null
+++ b/java/com/android/incallui/bindings/PhoneNumberService.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.bindings;
+
+import android.graphics.Bitmap;
+
+/** Provides phone number lookup services. */
+public interface PhoneNumberService {
+
+ /**
+ * Get a phone number number asynchronously.
+ *
+ * @param phoneNumber The phone number to lookup.
+ * @param listener The listener to notify when the phone number lookup is complete.
+ * @param imageListener The listener to notify when the image lookup is complete.
+ */
+ void getPhoneNumberInfo(
+ String phoneNumber,
+ NumberLookupListener listener,
+ ImageLookupListener imageListener,
+ boolean isIncoming);
+
+ interface NumberLookupListener {
+
+ /**
+ * Callback when a phone number has been looked up.
+ *
+ * @param info The looked up information. Or (@literal null} if there are no results.
+ */
+ void onPhoneNumberInfoComplete(PhoneNumberInfo info);
+ }
+
+ interface ImageLookupListener {
+
+ /**
+ * Callback when a image has been fetched.
+ *
+ * @param bitmap The fetched image.
+ */
+ void onImageFetchComplete(Bitmap bitmap);
+ }
+
+ interface PhoneNumberInfo {
+
+ String getDisplayName();
+
+ String getNumber();
+
+ int getPhoneType();
+
+ String getPhoneLabel();
+
+ String getNormalizedNumber();
+
+ String getImageUrl();
+
+ String getLookupKey();
+
+ boolean isBusiness();
+
+ int getLookupSource();
+ }
+}
diff --git a/java/com/android/incallui/call/CallList.java b/java/com/android/incallui/call/CallList.java
new file mode 100644
index 000000000..862c71cf9
--- /dev/null
+++ b/java/com/android/incallui/call/CallList.java
@@ -0,0 +1,763 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.call;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Trace;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.os.BuildCompat;
+import android.telecom.Call;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
+import android.util.ArrayMap;
+import com.android.contacts.common.GeoUtil;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.blocking.FilteredNumbersUtil;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.shortcuts.ShortcutUsageReporter;
+import com.android.dialer.spam.Spam;
+import com.android.dialer.spam.SpamBindings;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.call.DialerCall.State;
+import com.android.incallui.latencyreport.LatencyReport;
+import com.android.incallui.util.TelecomCallUtil;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Maintains the list of active calls and notifies interested classes of changes to the call list as
+ * they are received from the telephony stack. Primary listener of changes to this class is
+ * InCallPresenter.
+ */
+public class CallList implements DialerCallDelegate {
+
+ private static final int DISCONNECTED_CALL_SHORT_TIMEOUT_MS = 200;
+ private static final int DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS = 2000;
+ private static final int DISCONNECTED_CALL_LONG_TIMEOUT_MS = 5000;
+
+ private static final int EVENT_DISCONNECTED_TIMEOUT = 1;
+
+ private static CallList sInstance = new CallList();
+
+ private final Map<String, DialerCall> mCallById = new ArrayMap<>();
+ private final Map<android.telecom.Call, DialerCall> mCallByTelecomCall = new ArrayMap<>();
+
+ /**
+ * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is load factor before
+ * resizing, 1 means we only expect a single thread to access the map so make only a single shard
+ */
+ private final Set<Listener> mListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
+
+ private final Set<DialerCall> mPendingDisconnectCalls =
+ Collections.newSetFromMap(new ConcurrentHashMap<DialerCall, Boolean>(8, 0.9f, 1));
+ /** Handles the timeout for destroying disconnected calls. */
+ private final Handler mHandler =
+ new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case EVENT_DISCONNECTED_TIMEOUT:
+ LogUtil.d("CallList.handleMessage", "EVENT_DISCONNECTED_TIMEOUT ", msg.obj);
+ finishDisconnectedCall((DialerCall) msg.obj);
+ break;
+ default:
+ LogUtil.e("CallList.handleMessage", "Message not expected: " + msg.what);
+ break;
+ }
+ }
+ };
+
+ /**
+ * USED ONLY FOR TESTING Testing-only constructor. Instance should only be acquired through
+ * getInstance().
+ */
+ @VisibleForTesting
+ public CallList() {}
+
+ /** Static singleton accessor method. */
+ public static CallList getInstance() {
+ return sInstance;
+ }
+
+ public void onCallAdded(
+ final Context context, final android.telecom.Call telecomCall, LatencyReport latencyReport) {
+ Trace.beginSection("onCallAdded");
+ final DialerCall call =
+ new DialerCall(context, this, telecomCall, latencyReport, true /* registerCallback */);
+ final DialerCallListenerImpl dialerCallListener = new DialerCallListenerImpl(call);
+ call.addListener(dialerCallListener);
+ LogUtil.d("CallList.onCallAdded", "callState=" + call.getState());
+ if (Spam.get(context).isSpamEnabled()) {
+ String number = TelecomCallUtil.getNumber(telecomCall);
+ Spam.get(context)
+ .checkSpamStatus(
+ number,
+ null,
+ new SpamBindings.Listener() {
+ @Override
+ public void onComplete(boolean isSpam) {
+ if (isSpam) {
+ if (call.getState() != DialerCall.State.INCOMING
+ && call.getState() != DialerCall.State.CALL_WAITING) {
+ LogUtil.i(
+ "CallList.onCallAdded",
+ "marking spam call as not spam because it's not an incoming call");
+ isSpam = false;
+ } else if (isPotentialEmergencyCallback(context, call)) {
+ LogUtil.i(
+ "CallList.onCallAdded",
+ "marking spam call as not spam because an emergency call was made on this"
+ + " device recently");
+ isSpam = false;
+ }
+ }
+
+ Logger.get(context)
+ .logCallImpression(
+ isSpam
+ ? DialerImpression.Type.INCOMING_SPAM_CALL
+ : DialerImpression.Type.INCOMING_NON_SPAM_CALL,
+ call.getUniqueCallId(),
+ call.getTimeAddedMs());
+ call.setSpam(isSpam);
+ dialerCallListener.onDialerCallUpdate();
+ }
+ });
+
+ updateUserMarkedSpamStatus(call, context, number, dialerCallListener);
+ }
+
+ FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler =
+ new FilteredNumberAsyncQueryHandler(context);
+
+ filteredNumberAsyncQueryHandler.isBlockedNumber(
+ new FilteredNumberAsyncQueryHandler.OnCheckBlockedListener() {
+ @Override
+ public void onCheckComplete(Integer id) {
+ if (id != null && id != FilteredNumberAsyncQueryHandler.INVALID_ID) {
+ call.setBlockedStatus(true);
+ dialerCallListener.onDialerCallUpdate();
+ }
+ }
+ },
+ call.getNumber(),
+ GeoUtil.getCurrentCountryIso(context));
+
+ if (call.getState() == DialerCall.State.INCOMING
+ || call.getState() == DialerCall.State.CALL_WAITING) {
+ onIncoming(call);
+ } else {
+ dialerCallListener.onDialerCallUpdate();
+ }
+
+ if (call.getState() != State.INCOMING) {
+ // Only report outgoing calls
+ ShortcutUsageReporter.onOutgoingCallAdded(context, call.getNumber());
+ }
+
+ Trace.endSection();
+ }
+
+ private static boolean isPotentialEmergencyCallback(Context context, DialerCall call) {
+ if (BuildCompat.isAtLeastO()) {
+ return call.isPotentialEmergencyCallback();
+ } else {
+ long timestampMillis = FilteredNumbersUtil.getLastEmergencyCallTimeMillis(context);
+ return call.isInEmergencyCallbackWindow(timestampMillis);
+ }
+ }
+
+ @Override
+ public DialerCall getDialerCallFromTelecomCall(Call telecomCall) {
+ return mCallByTelecomCall.get(telecomCall);
+ }
+
+ public void updateUserMarkedSpamStatus(
+ final DialerCall call,
+ final Context context,
+ String number,
+ final DialerCallListenerImpl dialerCallListener) {
+
+ Spam.get(context)
+ .checkUserMarkedNonSpamStatus(
+ number,
+ null,
+ new SpamBindings.Listener() {
+ @Override
+ public void onComplete(boolean isInUserWhiteList) {
+ call.setIsInUserWhiteList(isInUserWhiteList);
+ }
+ });
+
+ Spam.get(context)
+ .checkGlobalSpamListStatus(
+ number,
+ null,
+ new SpamBindings.Listener() {
+ @Override
+ public void onComplete(boolean isInGlobalSpamList) {
+ call.setIsInGlobalSpamList(isInGlobalSpamList);
+ }
+ });
+
+ Spam.get(context)
+ .checkUserMarkedSpamStatus(
+ number,
+ null,
+ new SpamBindings.Listener() {
+ @Override
+ public void onComplete(boolean isInUserSpamList) {
+ call.setIsInUserSpamList(isInUserSpamList);
+ }
+ });
+ }
+
+ public void onCallRemoved(Context context, android.telecom.Call telecomCall) {
+ if (mCallByTelecomCall.containsKey(telecomCall)) {
+ DialerCall call = mCallByTelecomCall.get(telecomCall);
+ Assert.checkArgument(!call.isExternalCall());
+
+ // Don't log an already logged call. logCall() might be called multiple times
+ // for the same call due to b/24109437.
+ if (call.getLogState() != null && !call.getLogState().isLogged) {
+ getLegacyBindings(context).logCall(call);
+ call.getLogState().isLogged = true;
+ }
+
+ if (updateCallInMap(call)) {
+ LogUtil.w(
+ "CallList.onCallRemoved", "Removing call not previously disconnected " + call.getId());
+ }
+ }
+ }
+
+ InCallUiLegacyBindings getLegacyBindings(Context context) {
+ Objects.requireNonNull(context);
+
+ Context application = context.getApplicationContext();
+ InCallUiLegacyBindings legacyInstance = null;
+ if (application instanceof InCallUiLegacyBindingsFactory) {
+ legacyInstance = ((InCallUiLegacyBindingsFactory) application).newInCallUiLegacyBindings();
+ }
+
+ if (legacyInstance == null) {
+ legacyInstance = new InCallUiLegacyBindingsStub();
+ }
+ return legacyInstance;
+ }
+
+ /**
+ * Handles the case where an internal call has become an exteral call. We need to
+ *
+ * @param context
+ * @param telecomCall
+ */
+ public void onInternalCallMadeExternal(Context context, android.telecom.Call telecomCall) {
+
+ if (mCallByTelecomCall.containsKey(telecomCall)) {
+ DialerCall call = mCallByTelecomCall.get(telecomCall);
+
+ // Don't log an already logged call. logCall() might be called multiple times
+ // for the same call due to b/24109437.
+ if (call.getLogState() != null && !call.getLogState().isLogged) {
+ getLegacyBindings(context).logCall(call);
+ call.getLogState().isLogged = true;
+ }
+
+ // When removing a call from the call list because it became an external call, we need to
+ // ensure the callback is unregistered -- this is normally only done when calls disconnect.
+ // However, the call won't be disconnected in this case. Also, logic in updateCallInMap
+ // would just re-add the call anyways.
+ call.unregisterCallback();
+ mCallById.remove(call.getId());
+ mCallByTelecomCall.remove(telecomCall);
+ }
+ }
+
+ /** Called when a single call has changed. */
+ private void onIncoming(DialerCall call) {
+ if (updateCallInMap(call)) {
+ LogUtil.i("CallList.onIncoming", String.valueOf(call));
+ }
+
+ for (Listener listener : mListeners) {
+ listener.onIncomingCall(call);
+ }
+ }
+
+ public void addListener(@NonNull Listener listener) {
+ Objects.requireNonNull(listener);
+
+ mListeners.add(listener);
+
+ // Let the listener know about the active calls immediately.
+ listener.onCallListChange(this);
+ }
+
+ public void removeListener(@Nullable Listener listener) {
+ if (listener != null) {
+ mListeners.remove(listener);
+ }
+ }
+
+ /**
+ * TODO: Change so that this function is not needed. Instead of assuming there is an active call,
+ * the code should rely on the status of a specific DialerCall and allow the presenters to update
+ * the DialerCall object when the active call changes.
+ */
+ public DialerCall getIncomingOrActive() {
+ DialerCall retval = getIncomingCall();
+ if (retval == null) {
+ retval = getActiveCall();
+ }
+ return retval;
+ }
+
+ public DialerCall getOutgoingOrActive() {
+ DialerCall retval = getOutgoingCall();
+ if (retval == null) {
+ retval = getActiveCall();
+ }
+ return retval;
+ }
+
+ /** A call that is waiting for {@link PhoneAccount} selection */
+ public DialerCall getWaitingForAccountCall() {
+ return getFirstCallWithState(DialerCall.State.SELECT_PHONE_ACCOUNT);
+ }
+
+ public DialerCall getPendingOutgoingCall() {
+ return getFirstCallWithState(DialerCall.State.CONNECTING);
+ }
+
+ public DialerCall getOutgoingCall() {
+ DialerCall call = getFirstCallWithState(DialerCall.State.DIALING);
+ if (call == null) {
+ call = getFirstCallWithState(DialerCall.State.REDIALING);
+ }
+ if (call == null) {
+ call = getFirstCallWithState(DialerCall.State.PULLING);
+ }
+ return call;
+ }
+
+ public DialerCall getActiveCall() {
+ return getFirstCallWithState(DialerCall.State.ACTIVE);
+ }
+
+ public DialerCall getSecondActiveCall() {
+ return getCallWithState(DialerCall.State.ACTIVE, 1);
+ }
+
+ public DialerCall getBackgroundCall() {
+ return getFirstCallWithState(DialerCall.State.ONHOLD);
+ }
+
+ public DialerCall getDisconnectedCall() {
+ return getFirstCallWithState(DialerCall.State.DISCONNECTED);
+ }
+
+ public DialerCall getDisconnectingCall() {
+ return getFirstCallWithState(DialerCall.State.DISCONNECTING);
+ }
+
+ public DialerCall getSecondBackgroundCall() {
+ return getCallWithState(DialerCall.State.ONHOLD, 1);
+ }
+
+ public DialerCall getActiveOrBackgroundCall() {
+ DialerCall call = getActiveCall();
+ if (call == null) {
+ call = getBackgroundCall();
+ }
+ return call;
+ }
+
+ public DialerCall getIncomingCall() {
+ DialerCall call = getFirstCallWithState(DialerCall.State.INCOMING);
+ if (call == null) {
+ call = getFirstCallWithState(DialerCall.State.CALL_WAITING);
+ }
+
+ return call;
+ }
+
+ public DialerCall getFirstCall() {
+ DialerCall result = getIncomingCall();
+ if (result == null) {
+ result = getPendingOutgoingCall();
+ }
+ if (result == null) {
+ result = getOutgoingCall();
+ }
+ if (result == null) {
+ result = getFirstCallWithState(DialerCall.State.ACTIVE);
+ }
+ if (result == null) {
+ result = getDisconnectingCall();
+ }
+ if (result == null) {
+ result = getDisconnectedCall();
+ }
+ return result;
+ }
+
+ public boolean hasLiveCall() {
+ DialerCall call = getFirstCall();
+ return call != null && call != getDisconnectingCall() && call != getDisconnectedCall();
+ }
+
+ /**
+ * Returns the first call found in the call map with the upgrade to video modification state.
+ *
+ * @return The first call with the upgrade to video state.
+ */
+ public DialerCall getVideoUpgradeRequestCall() {
+ for (DialerCall call : mCallById.values()) {
+ if (call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ return call;
+ }
+ }
+ return null;
+ }
+
+ public DialerCall getCallById(String callId) {
+ return mCallById.get(callId);
+ }
+
+ /** Returns first call found in the call map with the specified state. */
+ public DialerCall getFirstCallWithState(int state) {
+ return getCallWithState(state, 0);
+ }
+
+ /**
+ * Returns the [position]th call found in the call map with the specified state. TODO: Improve
+ * this logic to sort by call time.
+ */
+ public DialerCall getCallWithState(int state, int positionToFind) {
+ DialerCall retval = null;
+ int position = 0;
+ for (DialerCall call : mCallById.values()) {
+ if (call.getState() == state) {
+ if (position >= positionToFind) {
+ retval = call;
+ break;
+ } else {
+ position++;
+ }
+ }
+ }
+
+ return retval;
+ }
+
+ /**
+ * This is called when the service disconnects, either expectedly or unexpectedly. For the
+ * expected case, it's because we have no calls left. For the unexpected case, it is likely a
+ * crash of phone and we need to clean up our calls manually. Without phone, there can be no
+ * active calls, so this is relatively safe thing to do.
+ */
+ public void clearOnDisconnect() {
+ for (DialerCall call : mCallById.values()) {
+ final int state = call.getState();
+ if (state != DialerCall.State.IDLE
+ && state != DialerCall.State.INVALID
+ && state != DialerCall.State.DISCONNECTED) {
+
+ call.setState(DialerCall.State.DISCONNECTED);
+ call.setDisconnectCause(new DisconnectCause(DisconnectCause.UNKNOWN));
+ updateCallInMap(call);
+ }
+ }
+ notifyGenericListeners();
+ }
+
+ /**
+ * Called when the user has dismissed an error dialog. This indicates acknowledgement of the
+ * disconnect cause, and that any pending disconnects should immediately occur.
+ */
+ public void onErrorDialogDismissed() {
+ final Iterator<DialerCall> iterator = mPendingDisconnectCalls.iterator();
+ while (iterator.hasNext()) {
+ DialerCall call = iterator.next();
+ iterator.remove();
+ finishDisconnectedCall(call);
+ }
+ }
+
+ /**
+ * Processes an update for a single call.
+ *
+ * @param call The call to update.
+ */
+ private void onUpdateCall(DialerCall call) {
+ LogUtil.d("CallList.onUpdateCall", String.valueOf(call));
+ if (!mCallById.containsKey(call.getId()) && call.isExternalCall()) {
+ // When a regular call becomes external, it is removed from the call list, and there may be
+ // pending updates to Telecom which are queued up on the Telecom call's handler which we no
+ // longer wish to cause updates to the call in the CallList. Bail here if the list of tracked
+ // calls doesn't contain the call which received the update.
+ return;
+ }
+
+ if (updateCallInMap(call)) {
+ LogUtil.i("CallList.onUpdateCall", String.valueOf(call));
+ }
+ }
+
+ /**
+ * Sends a generic notification to all listeners that something has changed. It is up to the
+ * listeners to call back to determine what changed.
+ */
+ private void notifyGenericListeners() {
+ for (Listener listener : mListeners) {
+ listener.onCallListChange(this);
+ }
+ }
+
+ private void notifyListenersOfDisconnect(DialerCall call) {
+ for (Listener listener : mListeners) {
+ listener.onDisconnect(call);
+ }
+ }
+
+ /**
+ * Updates the call entry in the local map.
+ *
+ * @return false if no call previously existed and no call was added, otherwise true.
+ */
+ private boolean updateCallInMap(DialerCall call) {
+ Objects.requireNonNull(call);
+
+ boolean updated = false;
+
+ if (call.getState() == DialerCall.State.DISCONNECTED) {
+ // update existing (but do not add!!) disconnected calls
+ if (mCallById.containsKey(call.getId())) {
+ // For disconnected calls, we want to keep them alive for a few seconds so that the
+ // UI has a chance to display anything it needs when a call is disconnected.
+
+ // Set up a timer to destroy the call after X seconds.
+ final Message msg = mHandler.obtainMessage(EVENT_DISCONNECTED_TIMEOUT, call);
+ mHandler.sendMessageDelayed(msg, getDelayForDisconnect(call));
+ mPendingDisconnectCalls.add(call);
+
+ mCallById.put(call.getId(), call);
+ mCallByTelecomCall.put(call.getTelecomCall(), call);
+ updated = true;
+ }
+ } else if (!isCallDead(call)) {
+ mCallById.put(call.getId(), call);
+ mCallByTelecomCall.put(call.getTelecomCall(), call);
+ updated = true;
+ } else if (mCallById.containsKey(call.getId())) {
+ mCallById.remove(call.getId());
+ mCallByTelecomCall.remove(call.getTelecomCall());
+ updated = true;
+ }
+
+ return updated;
+ }
+
+ private int getDelayForDisconnect(DialerCall call) {
+ if (call.getState() != DialerCall.State.DISCONNECTED) {
+ throw new IllegalStateException();
+ }
+
+ final int cause = call.getDisconnectCause().getCode();
+ final int delay;
+ switch (cause) {
+ case DisconnectCause.LOCAL:
+ delay = DISCONNECTED_CALL_SHORT_TIMEOUT_MS;
+ break;
+ case DisconnectCause.REMOTE:
+ case DisconnectCause.ERROR:
+ delay = DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS;
+ break;
+ case DisconnectCause.REJECTED:
+ case DisconnectCause.MISSED:
+ case DisconnectCause.CANCELED:
+ // no delay for missed/rejected incoming calls and canceled outgoing calls.
+ delay = 0;
+ break;
+ default:
+ delay = DISCONNECTED_CALL_LONG_TIMEOUT_MS;
+ break;
+ }
+
+ return delay;
+ }
+
+ private boolean isCallDead(DialerCall call) {
+ final int state = call.getState();
+ return DialerCall.State.IDLE == state || DialerCall.State.INVALID == state;
+ }
+
+ /** Sets up a call for deletion and notifies listeners of change. */
+ private void finishDisconnectedCall(DialerCall call) {
+ if (mPendingDisconnectCalls.contains(call)) {
+ mPendingDisconnectCalls.remove(call);
+ }
+ call.setState(DialerCall.State.IDLE);
+ updateCallInMap(call);
+ notifyGenericListeners();
+ }
+
+ /**
+ * Notifies all video calls of a change in device orientation.
+ *
+ * @param rotation The new rotation angle (in degrees).
+ */
+ public void notifyCallsOfDeviceRotation(int rotation) {
+ for (DialerCall call : mCallById.values()) {
+ // First, ensure that the call videoState has video enabled (there is no need to set
+ // device orientation on a voice call which has not yet been upgraded to video).
+ // Second, ensure a VideoCall is set on the call so that the change can be sent to the
+ // provider (a VideoCall can be present for a call that does not currently have video,
+ // but can be upgraded to video).
+
+ // NOTE: is it necessary to use this order because getVideoCall references the class
+ // VideoProfile which is not available on APIs <23 (M).
+ if (VideoUtils.isVideoCall(call) && call.getVideoCall() != null) {
+ call.getVideoCall().setDeviceOrientation(rotation);
+ }
+ }
+ }
+
+ public void onInCallUiShown(boolean forFullScreenIntent) {
+ for (DialerCall call : mCallById.values()) {
+ call.getLatencyReport().onInCallUiShown(forFullScreenIntent);
+ }
+ }
+
+ /** Listener interface for any class that wants to be notified of changes to the call list. */
+ public interface Listener {
+
+ /**
+ * Called when a new incoming call comes in. This is the only method that gets called for
+ * incoming calls. Listeners that want to perform an action on incoming call should respond in
+ * this method because {@link #onCallListChange} does not automatically get called for incoming
+ * calls.
+ */
+ void onIncomingCall(DialerCall call);
+
+ /**
+ * Called when a new modify call request comes in This is the only method that gets called for
+ * modify requests.
+ */
+ void onUpgradeToVideo(DialerCall call);
+
+ /** Called when the session modification state of a call changes. */
+ void onSessionModificationStateChange(@SessionModificationState int newState);
+
+ /**
+ * Called anytime there are changes to the call list. The change can be switching call states,
+ * updating information, etc. This method will NOT be called for new incoming calls and for
+ * calls that switch to disconnected state. Listeners must add actions to those method
+ * implementations if they want to deal with those actions.
+ */
+ void onCallListChange(CallList callList);
+
+ /**
+ * Called when a call switches to the disconnected state. This is the only method that will get
+ * called upon disconnection.
+ */
+ void onDisconnect(DialerCall call);
+
+ void onWiFiToLteHandover(DialerCall call);
+
+ /**
+ * Called when a user is in a video call and the call is unable to be handed off successfully to
+ * WiFi
+ */
+ void onHandoverToWifiFailed(DialerCall call);
+ }
+
+ private class DialerCallListenerImpl implements DialerCallListener {
+
+ private final DialerCall mCall;
+
+ DialerCallListenerImpl(DialerCall call) {
+ Assert.isNotNull(call);
+ mCall = call;
+ }
+
+ @Override
+ public void onDialerCallDisconnect() {
+ if (updateCallInMap(mCall)) {
+ LogUtil.i("DialerCallListenerImpl.onDialerCallDisconnect", String.valueOf(mCall));
+ // notify those listening for all disconnects
+ notifyListenersOfDisconnect(mCall);
+ }
+ }
+
+ @Override
+ public void onDialerCallUpdate() {
+ Trace.beginSection("onUpdate");
+ onUpdateCall(mCall);
+ notifyGenericListeners();
+ Trace.endSection();
+ }
+
+ @Override
+ public void onDialerCallChildNumberChange() {}
+
+ @Override
+ public void onDialerCallLastForwardedNumberChange() {}
+
+ @Override
+ public void onDialerCallUpgradeToVideo() {
+ for (Listener listener : mListeners) {
+ listener.onUpgradeToVideo(mCall);
+ }
+ }
+
+ @Override
+ public void onWiFiToLteHandover() {
+ for (Listener listener : mListeners) {
+ listener.onWiFiToLteHandover(mCall);
+ }
+ }
+
+ @Override
+ public void onHandoverToWifiFailure() {
+ for (Listener listener : mListeners) {
+ listener.onHandoverToWifiFailed(mCall);
+ }
+ }
+
+ @Override
+ public void onDialerCallSessionModificationStateChange(@SessionModificationState int state) {
+ for (Listener listener : mListeners) {
+ listener.onSessionModificationStateChange(state);
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/call/DialerCall.java b/java/com/android/incallui/call/DialerCall.java
new file mode 100644
index 000000000..bd8f006dd
--- /dev/null
+++ b/java/com/android/incallui/call/DialerCall.java
@@ -0,0 +1,1401 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.call;
+
+import android.content.Context;
+import android.hardware.camera2.CameraCharacteristics;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Trace;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import android.telecom.Call;
+import android.telecom.Call.Details;
+import android.telecom.Connection;
+import android.telecom.DisconnectCause;
+import android.telecom.GatewayInfo;
+import android.telecom.InCallService.VideoCall;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.StatusHints;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import com.android.contacts.common.compat.CallCompat;
+import com.android.contacts.common.compat.TelephonyManagerCompat;
+import com.android.contacts.common.compat.telecom.TelecomManagerCompat;
+import com.android.dialer.callintent.CallIntentParser;
+import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.callintent.nano.CallSpecificAppData;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.nano.ContactLookupResult;
+import com.android.dialer.util.CallUtil;
+import com.android.incallui.latencyreport.LatencyReport;
+import com.android.incallui.util.TelecomCallUtil;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+
+/** Describes a single call and its state. */
+public class DialerCall {
+
+ public static final int CALL_HISTORY_STATUS_UNKNOWN = 0;
+ public static final int CALL_HISTORY_STATUS_PRESENT = 1;
+ public static final int CALL_HISTORY_STATUS_NOT_PRESENT = 2;
+ private static final String ID_PREFIX = "DialerCall_";
+ private static final String CONFIG_EMERGENCY_CALLBACK_WINDOW_MILLIS =
+ "emergency_callback_window_millis";
+ private static int sIdCounter = 0;
+
+ /**
+ * The unique call ID for every call. This will help us to identify each call and allow us the
+ * ability to stitch impressions to calls if needed.
+ */
+ private final String uniqueCallId = UUID.randomUUID().toString();
+
+ private final Call mTelecomCall;
+ private final LatencyReport mLatencyReport;
+ private final String mId;
+ private final List<String> mChildCallIds = new ArrayList<>();
+ private final VideoSettings mVideoSettings = new VideoSettings();
+ private final LogState mLogState = new LogState();
+ private final Context mContext;
+ private final DialerCallDelegate mDialerCallDelegate;
+ private final List<DialerCallListener> mListeners = new CopyOnWriteArrayList<>();
+ private final List<CannedTextResponsesLoadedListener> mCannedTextResponsesLoadedListeners =
+ new CopyOnWriteArrayList<>();
+
+ private boolean mIsEmergencyCall;
+ private Uri mHandle;
+ private int mState = State.INVALID;
+ private DisconnectCause mDisconnectCause;
+
+ private boolean hasShownWiFiToLteHandoverToast;
+ private boolean doNotShowDialogForHandoffToWifiFailure;
+
+ @SessionModificationState private int mSessionModificationState;
+ private int mVideoState;
+ /** mRequestedVideoState is used to store requested upgrade / downgrade video state */
+ private int mRequestedVideoState = VideoProfile.STATE_AUDIO_ONLY;
+
+ private InCallVideoCallCallback mVideoCallCallback;
+ private boolean mIsVideoCallCallbackRegistered;
+ private String mChildNumber;
+ private String mLastForwardedNumber;
+ private String mCallSubject;
+ private PhoneAccountHandle mPhoneAccountHandle;
+ @CallHistoryStatus private int mCallHistoryStatus = CALL_HISTORY_STATUS_UNKNOWN;
+ private boolean mIsSpam;
+ private boolean mIsBlocked;
+ private boolean isInUserSpamList;
+ private boolean isInUserWhiteList;
+ private boolean isInGlobalSpamList;
+ private boolean didShowCameraPermission;
+ private String callProviderLabel;
+ private String callbackNumber;
+
+ public static String getNumberFromHandle(Uri handle) {
+ return handle == null ? "" : handle.getSchemeSpecificPart();
+ }
+
+ /**
+ * Whether the call is put on hold by remote party. This is different than the {@link
+ * State.ONHOLD} state which indicates that the call is being held locally on the device.
+ */
+ private boolean isRemotelyHeld;
+
+ /**
+ * Indicates whether the phone account associated with this call supports specifying a call
+ * subject.
+ */
+ private boolean mIsCallSubjectSupported;
+
+ private final Call.Callback mTelecomCallCallback =
+ new Call.Callback() {
+ @Override
+ public void onStateChanged(Call call, int newState) {
+ LogUtil.v("TelecomCallCallback.onStateChanged", "call=" + call + " newState=" + newState);
+ update();
+ }
+
+ @Override
+ public void onParentChanged(Call call, Call newParent) {
+ LogUtil.v(
+ "TelecomCallCallback.onParentChanged", "call=" + call + " newParent=" + newParent);
+ update();
+ }
+
+ @Override
+ public void onChildrenChanged(Call call, List<Call> children) {
+ update();
+ }
+
+ @Override
+ public void onDetailsChanged(Call call, Call.Details details) {
+ LogUtil.v("TelecomCallCallback.onStateChanged", " call=" + call + " details=" + details);
+ update();
+ }
+
+ @Override
+ public void onCannedTextResponsesLoaded(Call call, List<String> cannedTextResponses) {
+ LogUtil.v(
+ "TelecomCallCallback.onStateChanged",
+ "call=" + call + " cannedTextResponses=" + cannedTextResponses);
+ for (CannedTextResponsesLoadedListener listener : mCannedTextResponsesLoadedListeners) {
+ listener.onCannedTextResponsesLoaded(DialerCall.this);
+ }
+ }
+
+ @Override
+ public void onPostDialWait(Call call, String remainingPostDialSequence) {
+ LogUtil.v(
+ "TelecomCallCallback.onStateChanged",
+ "call=" + call + " remainingPostDialSequence=" + remainingPostDialSequence);
+ update();
+ }
+
+ @Override
+ public void onVideoCallChanged(Call call, VideoCall videoCall) {
+ LogUtil.v(
+ "TelecomCallCallback.onStateChanged", "call=" + call + " videoCall=" + videoCall);
+ update();
+ }
+
+ @Override
+ public void onCallDestroyed(Call call) {
+ LogUtil.v("TelecomCallCallback.onStateChanged", "call=" + call);
+ call.unregisterCallback(this);
+ }
+
+ @Override
+ public void onConferenceableCallsChanged(Call call, List<Call> conferenceableCalls) {
+ LogUtil.v(
+ "DialerCall.onConferenceableCallsChanged",
+ "call %s, conferenceable calls: %d",
+ call,
+ conferenceableCalls.size());
+ update();
+ }
+
+ @Override
+ public void onConnectionEvent(android.telecom.Call call, String event, Bundle extras) {
+ LogUtil.v(
+ "DialerCall.onConnectionEvent",
+ "Call: " + call + ", Event: " + event + ", Extras: " + extras);
+ switch (event) {
+ // The Previous attempt to Merge two calls together has failed in Telecom. We must
+ // now update the UI to possibly re-enable the Merge button based on the number of
+ // currently conferenceable calls available or Connection Capabilities.
+ case android.telecom.Connection.EVENT_CALL_MERGE_FAILED:
+ update();
+ break;
+ case TelephonyManagerCompat.EVENT_HANDOVER_VIDEO_FROM_WIFI_TO_LTE:
+ notifyWiFiToLteHandover();
+ break;
+ case TelephonyManagerCompat.EVENT_HANDOVER_TO_WIFI_FAILED:
+ notifyHandoverToWifiFailed();
+ break;
+ case TelephonyManagerCompat.EVENT_CALL_REMOTELY_HELD:
+ isRemotelyHeld = true;
+ update();
+ break;
+ case TelephonyManagerCompat.EVENT_CALL_REMOTELY_UNHELD:
+ isRemotelyHeld = false;
+ update();
+ break;
+ default:
+ break;
+ }
+ }
+ };
+ private long mTimeAddedMs;
+
+ public DialerCall(
+ Context context,
+ DialerCallDelegate dialerCallDelegate,
+ Call telecomCall,
+ LatencyReport latencyReport,
+ boolean registerCallback) {
+ Assert.isNotNull(context);
+ mContext = context;
+ mDialerCallDelegate = dialerCallDelegate;
+ mTelecomCall = telecomCall;
+ mLatencyReport = latencyReport;
+ mId = ID_PREFIX + Integer.toString(sIdCounter++);
+
+ updateFromTelecomCall(registerCallback);
+
+ if (registerCallback) {
+ mTelecomCall.registerCallback(mTelecomCallCallback);
+ }
+
+ mTimeAddedMs = System.currentTimeMillis();
+ parseCallSpecificAppData();
+ }
+
+ private static int translateState(int state) {
+ switch (state) {
+ case Call.STATE_NEW:
+ case Call.STATE_CONNECTING:
+ return DialerCall.State.CONNECTING;
+ case Call.STATE_SELECT_PHONE_ACCOUNT:
+ return DialerCall.State.SELECT_PHONE_ACCOUNT;
+ case Call.STATE_DIALING:
+ return DialerCall.State.DIALING;
+ case Call.STATE_PULLING_CALL:
+ return DialerCall.State.PULLING;
+ case Call.STATE_RINGING:
+ return DialerCall.State.INCOMING;
+ case Call.STATE_ACTIVE:
+ return DialerCall.State.ACTIVE;
+ case Call.STATE_HOLDING:
+ return DialerCall.State.ONHOLD;
+ case Call.STATE_DISCONNECTED:
+ return DialerCall.State.DISCONNECTED;
+ case Call.STATE_DISCONNECTING:
+ return DialerCall.State.DISCONNECTING;
+ default:
+ return DialerCall.State.INVALID;
+ }
+ }
+
+ public static boolean areSame(DialerCall call1, DialerCall call2) {
+ if (call1 == null && call2 == null) {
+ return true;
+ } else if (call1 == null || call2 == null) {
+ return false;
+ }
+
+ // otherwise compare call Ids
+ return call1.getId().equals(call2.getId());
+ }
+
+ public static boolean areSameNumber(DialerCall call1, DialerCall call2) {
+ if (call1 == null && call2 == null) {
+ return true;
+ } else if (call1 == null || call2 == null) {
+ return false;
+ }
+
+ // otherwise compare call Numbers
+ return TextUtils.equals(call1.getNumber(), call2.getNumber());
+ }
+
+ public void addListener(DialerCallListener listener) {
+ Assert.isMainThread();
+ mListeners.add(listener);
+ }
+
+ public void removeListener(DialerCallListener listener) {
+ Assert.isMainThread();
+ mListeners.remove(listener);
+ }
+
+ public void addCannedTextResponsesLoadedListener(CannedTextResponsesLoadedListener listener) {
+ Assert.isMainThread();
+ mCannedTextResponsesLoadedListeners.add(listener);
+ }
+
+ public void removeCannedTextResponsesLoadedListener(CannedTextResponsesLoadedListener listener) {
+ Assert.isMainThread();
+ mCannedTextResponsesLoadedListeners.remove(listener);
+ }
+
+ public void notifyWiFiToLteHandover() {
+ LogUtil.i("DialerCall.notifyWiFiToLteHandover", "");
+ for (DialerCallListener listener : mListeners) {
+ listener.onWiFiToLteHandover();
+ }
+ }
+
+ public void notifyHandoverToWifiFailed() {
+ LogUtil.i("DialerCall.notifyHandoverToWifiFailed", "");
+ for (DialerCallListener listener : mListeners) {
+ listener.onHandoverToWifiFailure();
+ }
+ }
+
+ /* package-private */ Call getTelecomCall() {
+ return mTelecomCall;
+ }
+
+ public StatusHints getStatusHints() {
+ return mTelecomCall.getDetails().getStatusHints();
+ }
+
+ /**
+ * @return video settings of the call, null if the call is not a video call.
+ * @see VideoProfile
+ */
+ public VideoSettings getVideoSettings() {
+ return mVideoSettings;
+ }
+
+ private void update() {
+ Trace.beginSection("Update");
+ int oldState = getState();
+ // We want to potentially register a video call callback here.
+ updateFromTelecomCall(true /* registerCallback */);
+ if (oldState != getState() && getState() == DialerCall.State.DISCONNECTED) {
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallDisconnect();
+ }
+ } else {
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallUpdate();
+ }
+ }
+ Trace.endSection();
+ }
+
+ private void updateFromTelecomCall(boolean registerCallback) {
+ LogUtil.v("DialerCall.updateFromTelecomCall", mTelecomCall.toString());
+ final int translatedState = translateState(mTelecomCall.getState());
+ if (mState != State.BLOCKED) {
+ setState(translatedState);
+ setDisconnectCause(mTelecomCall.getDetails().getDisconnectCause());
+ maybeCancelVideoUpgrade(mTelecomCall.getDetails().getVideoState());
+ }
+
+ if (registerCallback && mTelecomCall.getVideoCall() != null) {
+ if (mVideoCallCallback == null) {
+ mVideoCallCallback = new InCallVideoCallCallback(this);
+ }
+ mTelecomCall.getVideoCall().registerCallback(mVideoCallCallback);
+ mIsVideoCallCallbackRegistered = true;
+ }
+
+ mChildCallIds.clear();
+ final int numChildCalls = mTelecomCall.getChildren().size();
+ for (int i = 0; i < numChildCalls; i++) {
+ mChildCallIds.add(
+ mDialerCallDelegate
+ .getDialerCallFromTelecomCall(mTelecomCall.getChildren().get(i))
+ .getId());
+ }
+
+ // The number of conferenced calls can change over the course of the call, so use the
+ // maximum number of conferenced child calls as the metric for conference call usage.
+ mLogState.conferencedCalls = Math.max(numChildCalls, mLogState.conferencedCalls);
+
+ updateFromCallExtras(mTelecomCall.getDetails().getExtras());
+
+ // If the handle of the call has changed, update state for the call determining if it is an
+ // emergency call.
+ Uri newHandle = mTelecomCall.getDetails().getHandle();
+ if (!Objects.equals(mHandle, newHandle)) {
+ mHandle = newHandle;
+ updateEmergencyCallState();
+ }
+
+ // If the phone account handle of the call is set, cache capability bit indicating whether
+ // the phone account supports call subjects.
+ PhoneAccountHandle newPhoneAccountHandle = mTelecomCall.getDetails().getAccountHandle();
+ if (!Objects.equals(mPhoneAccountHandle, newPhoneAccountHandle)) {
+ mPhoneAccountHandle = newPhoneAccountHandle;
+
+ if (mPhoneAccountHandle != null) {
+ PhoneAccount phoneAccount =
+ mContext.getSystemService(TelecomManager.class).getPhoneAccount(mPhoneAccountHandle);
+ if (phoneAccount != null) {
+ mIsCallSubjectSupported =
+ phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_CALL_SUBJECT);
+ }
+ }
+ }
+
+ if (mSessionModificationState
+ == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
+ && isVideoCall()) {
+ // We find out in {@link InCallVideoCallCallback.onSessionModifyResponseReceived}
+ // whether the video upgrade request was accepted. We don't clear the session modification
+ // state right away though to avoid having the UI switch from video to voice to video.
+ // Once the underlying telecom call updates to video mode it's safe to clear the state.
+ LogUtil.i(
+ "DialerCall.updateFromTelecomCall",
+ "upgraded to video, clearing session modification state");
+ setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+ }
+
+ /**
+ * Tests corruption of the {@code callExtras} bundle by calling {@link
+ * Bundle#containsKey(String)}. If the bundle is corrupted a {@link IllegalArgumentException} will
+ * be thrown and caught by this function.
+ *
+ * @param callExtras the bundle to verify
+ * @return {@code true} if the bundle is corrupted, {@code false} otherwise.
+ */
+ protected boolean areCallExtrasCorrupted(Bundle callExtras) {
+ /**
+ * There's currently a bug in Telephony service (b/25613098) that could corrupt the extras
+ * bundle, resulting in a IllegalArgumentException while validating data under {@link
+ * Bundle#containsKey(String)}.
+ */
+ try {
+ callExtras.containsKey(Connection.EXTRA_CHILD_ADDRESS);
+ return false;
+ } catch (IllegalArgumentException e) {
+ LogUtil.e(
+ "DialerCall.areCallExtrasCorrupted", "callExtras is corrupted, ignoring exception", e);
+ return true;
+ }
+ }
+
+ protected void updateFromCallExtras(Bundle callExtras) {
+ if (callExtras == null || areCallExtrasCorrupted(callExtras)) {
+ /**
+ * If the bundle is corrupted, abandon information update as a work around. These are not
+ * critical for the dialer to function.
+ */
+ return;
+ }
+ // Check for a change in the child address and notify any listeners.
+ if (callExtras.containsKey(Connection.EXTRA_CHILD_ADDRESS)) {
+ String childNumber = callExtras.getString(Connection.EXTRA_CHILD_ADDRESS);
+ if (!Objects.equals(childNumber, mChildNumber)) {
+ mChildNumber = childNumber;
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallChildNumberChange();
+ }
+ }
+ }
+
+ // Last forwarded number comes in as an array of strings. We want to choose the
+ // last item in the array. The forwarding numbers arrive independently of when the
+ // call is originally set up, so we need to notify the the UI of the change.
+ if (callExtras.containsKey(Connection.EXTRA_LAST_FORWARDED_NUMBER)) {
+ ArrayList<String> lastForwardedNumbers =
+ callExtras.getStringArrayList(Connection.EXTRA_LAST_FORWARDED_NUMBER);
+
+ if (lastForwardedNumbers != null) {
+ String lastForwardedNumber = null;
+ if (!lastForwardedNumbers.isEmpty()) {
+ lastForwardedNumber = lastForwardedNumbers.get(lastForwardedNumbers.size() - 1);
+ }
+
+ if (!Objects.equals(lastForwardedNumber, mLastForwardedNumber)) {
+ mLastForwardedNumber = lastForwardedNumber;
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallLastForwardedNumberChange();
+ }
+ }
+ }
+ }
+
+ // DialerCall subject is present in the extras at the start of call, so we do not need to
+ // notify any other listeners of this.
+ if (callExtras.containsKey(Connection.EXTRA_CALL_SUBJECT)) {
+ String callSubject = callExtras.getString(Connection.EXTRA_CALL_SUBJECT);
+ if (!Objects.equals(mCallSubject, callSubject)) {
+ mCallSubject = callSubject;
+ }
+ }
+ }
+
+ /**
+ * Determines if a received upgrade to video request should be cancelled. This can happen if
+ * another InCall UI responds to the upgrade to video request.
+ *
+ * @param newVideoState The new video state.
+ */
+ private void maybeCancelVideoUpgrade(int newVideoState) {
+ boolean isVideoStateChanged = mVideoState != newVideoState;
+
+ if (mSessionModificationState
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST
+ && isVideoStateChanged) {
+
+ LogUtil.i("DialerCall.maybeCancelVideoUpgrade", "cancelling upgrade notification");
+ setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+ mVideoState = newVideoState;
+ }
+
+ public String getId() {
+ return mId;
+ }
+
+ public boolean hasShownWiFiToLteHandoverToast() {
+ return hasShownWiFiToLteHandoverToast;
+ }
+
+ public void setHasShownWiFiToLteHandoverToast() {
+ hasShownWiFiToLteHandoverToast = true;
+ }
+
+ public boolean showWifiHandoverAlertAsToast() {
+ return doNotShowDialogForHandoffToWifiFailure;
+ }
+
+ public void setDoNotShowDialogForHandoffToWifiFailure(boolean bool) {
+ doNotShowDialogForHandoffToWifiFailure = bool;
+ }
+
+ public long getTimeAddedMs() {
+ return mTimeAddedMs;
+ }
+
+ @Nullable
+ public String getNumber() {
+ return TelecomCallUtil.getNumber(mTelecomCall);
+ }
+
+ public void blockCall() {
+ mTelecomCall.reject(false, null);
+ setState(State.BLOCKED);
+ }
+
+ @Nullable
+ public Uri getHandle() {
+ return mTelecomCall == null ? null : mTelecomCall.getDetails().getHandle();
+ }
+
+ public boolean isEmergencyCall() {
+ return mIsEmergencyCall;
+ }
+
+ public boolean isPotentialEmergencyCallback() {
+ // The property PROPERTY_EMERGENCY_CALLBACK_MODE is only set for CDMA calls when the system
+ // is actually in emergency callback mode (ie data is disabled).
+ if (hasProperty(Details.PROPERTY_EMERGENCY_CALLBACK_MODE)) {
+ return true;
+ }
+ // We want to treat any incoming call that arrives a short time after an outgoing emergency call
+ // as a potential emergency callback.
+ if (getExtras() != null
+ && getExtras().getLong(TelecomManagerCompat.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS, 0)
+ > 0) {
+ long lastEmergencyCallMillis =
+ getExtras().getLong(TelecomManagerCompat.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS, 0);
+ if (isInEmergencyCallbackWindow(lastEmergencyCallMillis)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean isInEmergencyCallbackWindow(long timestampMillis) {
+ long emergencyCallbackWindowMillis =
+ ConfigProviderBindings.get(mContext)
+ .getLong(CONFIG_EMERGENCY_CALLBACK_WINDOW_MILLIS, TimeUnit.MINUTES.toMillis(5));
+ return System.currentTimeMillis() - timestampMillis < emergencyCallbackWindowMillis;
+ }
+
+ public int getState() {
+ if (mTelecomCall != null && mTelecomCall.getParent() != null) {
+ return State.CONFERENCED;
+ } else {
+ return mState;
+ }
+ }
+
+ public void setState(int state) {
+ mState = state;
+ if (mState == State.INCOMING) {
+ mLogState.isIncoming = true;
+ } else if (mState == State.DISCONNECTED) {
+ mLogState.duration =
+ getConnectTimeMillis() == 0 ? 0 : System.currentTimeMillis() - getConnectTimeMillis();
+ }
+ }
+
+ public int getNumberPresentation() {
+ return mTelecomCall == null ? -1 : mTelecomCall.getDetails().getHandlePresentation();
+ }
+
+ public int getCnapNamePresentation() {
+ return mTelecomCall == null ? -1 : mTelecomCall.getDetails().getCallerDisplayNamePresentation();
+ }
+
+ @Nullable
+ public String getCnapName() {
+ return mTelecomCall == null ? null : getTelecomCall().getDetails().getCallerDisplayName();
+ }
+
+ public Bundle getIntentExtras() {
+ return mTelecomCall.getDetails().getIntentExtras();
+ }
+
+ @Nullable
+ public Bundle getExtras() {
+ return mTelecomCall == null ? null : mTelecomCall.getDetails().getExtras();
+ }
+
+ /** @return The child number for the call, or {@code null} if none specified. */
+ public String getChildNumber() {
+ return mChildNumber;
+ }
+
+ /** @return The last forwarded number for the call, or {@code null} if none specified. */
+ public String getLastForwardedNumber() {
+ return mLastForwardedNumber;
+ }
+
+ /** @return The call subject, or {@code null} if none specified. */
+ public String getCallSubject() {
+ return mCallSubject;
+ }
+
+ /**
+ * @return {@code true} if the call's phone account supports call subjects, {@code false}
+ * otherwise.
+ */
+ public boolean isCallSubjectSupported() {
+ return mIsCallSubjectSupported;
+ }
+
+ /** Returns call disconnect cause, defined by {@link DisconnectCause}. */
+ public DisconnectCause getDisconnectCause() {
+ if (mState == State.DISCONNECTED || mState == State.IDLE) {
+ return mDisconnectCause;
+ }
+
+ return new DisconnectCause(DisconnectCause.UNKNOWN);
+ }
+
+ public void setDisconnectCause(DisconnectCause disconnectCause) {
+ mDisconnectCause = disconnectCause;
+ mLogState.disconnectCause = mDisconnectCause;
+ }
+
+ /** Returns the possible text message responses. */
+ public List<String> getCannedSmsResponses() {
+ return mTelecomCall.getCannedTextResponses();
+ }
+
+ /** Checks if the call supports the given set of capabilities supplied as a bit mask. */
+ public boolean can(int capabilities) {
+ int supportedCapabilities = mTelecomCall.getDetails().getCallCapabilities();
+
+ if ((capabilities & Call.Details.CAPABILITY_MERGE_CONFERENCE) != 0) {
+ // We allow you to merge if the capabilities allow it or if it is a call with
+ // conferenceable calls.
+ if (mTelecomCall.getConferenceableCalls().isEmpty()
+ && ((Call.Details.CAPABILITY_MERGE_CONFERENCE & supportedCapabilities) == 0)) {
+ // Cannot merge calls if there are no calls to merge with.
+ return false;
+ }
+ capabilities &= ~Call.Details.CAPABILITY_MERGE_CONFERENCE;
+ }
+ return (capabilities == (capabilities & supportedCapabilities));
+ }
+
+ public boolean hasProperty(int property) {
+ return mTelecomCall.getDetails().hasProperty(property);
+ }
+
+ public String getUniqueCallId() {
+ return uniqueCallId;
+ }
+
+ /** Gets the time when the call first became active. */
+ public long getConnectTimeMillis() {
+ return mTelecomCall.getDetails().getConnectTimeMillis();
+ }
+
+ public boolean isConferenceCall() {
+ return hasProperty(Call.Details.PROPERTY_CONFERENCE);
+ }
+
+ @Nullable
+ public GatewayInfo getGatewayInfo() {
+ return mTelecomCall == null ? null : mTelecomCall.getDetails().getGatewayInfo();
+ }
+
+ @Nullable
+ public PhoneAccountHandle getAccountHandle() {
+ return mTelecomCall == null ? null : mTelecomCall.getDetails().getAccountHandle();
+ }
+
+ /**
+ * @return The {@link VideoCall} instance associated with the {@link Call}. Will return {@code
+ * null} until {@link #updateFromTelecomCall(boolean)} has registered a valid callback on the
+ * {@link VideoCall}.
+ */
+ public VideoCall getVideoCall() {
+ return mTelecomCall == null || !mIsVideoCallCallbackRegistered
+ ? null
+ : mTelecomCall.getVideoCall();
+ }
+
+ public List<String> getChildCallIds() {
+ return mChildCallIds;
+ }
+
+ public String getParentId() {
+ Call parentCall = mTelecomCall.getParent();
+ if (parentCall != null) {
+ return mDialerCallDelegate.getDialerCallFromTelecomCall(parentCall).getId();
+ }
+ return null;
+ }
+
+ public int getVideoState() {
+ return mTelecomCall.getDetails().getVideoState();
+ }
+
+ public boolean isVideoCall() {
+ return CallUtil.isVideoEnabled(mContext) && VideoUtils.isVideoCall(getVideoState());
+ }
+
+ /**
+ * Determines if the call handle is an emergency number or not and caches the result to avoid
+ * repeated calls to isEmergencyNumber.
+ */
+ private void updateEmergencyCallState() {
+ mIsEmergencyCall = TelecomCallUtil.isEmergencyCall(mTelecomCall);
+ }
+
+ /**
+ * Gets the video state which was requested via a session modification request.
+ *
+ * @return The video state.
+ */
+ public int getRequestedVideoState() {
+ return mRequestedVideoState;
+ }
+
+ /**
+ * Handles incoming session modification requests. Stores the pending video request and sets the
+ * session modification state to {@link
+ * DialerCall#SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST} so that we can keep
+ * track of the fact the request was received. Only upgrade requests require user confirmation and
+ * will be handled by this method. The remote user can turn off their own camera without
+ * confirmation.
+ *
+ * @param videoState The requested video state.
+ */
+ public void setRequestedVideoState(int videoState) {
+ LogUtil.v("DialerCall.setRequestedVideoState", "videoState: " + videoState);
+ if (videoState == getVideoState()) {
+ LogUtil.e("DialerCall.setRequestedVideoState", "clearing session modification state");
+ setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ return;
+ }
+
+ mRequestedVideoState = videoState;
+ setSessionModificationState(
+ DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST);
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallUpgradeToVideo();
+ }
+
+ LogUtil.i(
+ "DialerCall.setRequestedVideoState",
+ "mSessionModificationState: %d, videoState: %d",
+ mSessionModificationState,
+ videoState);
+ update();
+ }
+
+ /**
+ * Gets the current video session modification state.
+ *
+ * @return The session modification state.
+ */
+ @SessionModificationState
+ public int getSessionModificationState() {
+ return mSessionModificationState;
+ }
+
+ /**
+ * Set the session modification state. Used to keep track of pending video session modification
+ * operations and to inform listeners of these changes.
+ *
+ * @param state the new session modification state.
+ */
+ public void setSessionModificationState(@SessionModificationState int state) {
+ boolean hasChanged = mSessionModificationState != state;
+ if (hasChanged) {
+ LogUtil.i(
+ "DialerCall.setSessionModificationState", "%d -> %d", mSessionModificationState, state);
+ mSessionModificationState = state;
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallSessionModificationStateChange(state);
+ }
+ }
+ }
+
+ public LogState getLogState() {
+ return mLogState;
+ }
+
+ /**
+ * Determines if the call is an external call.
+ *
+ * <p>An external call is one which does not exist locally for the {@link
+ * android.telecom.ConnectionService} it is associated with.
+ *
+ * <p>External calls are only supported in N and higher.
+ *
+ * @return {@code true} if the call is an external call, {@code false} otherwise.
+ */
+ public boolean isExternalCall() {
+ return VERSION.SDK_INT >= VERSION_CODES.N
+ && hasProperty(CallCompat.Details.PROPERTY_IS_EXTERNAL_CALL);
+ }
+
+ /**
+ * Determines if the external call is pullable.
+ *
+ * <p>An external call is one which does not exist locally for the {@link
+ * android.telecom.ConnectionService} it is associated with. An external call may be "pullable",
+ * which means that the user can request it be transferred to the current device.
+ *
+ * <p>External calls are only supported in N and higher.
+ *
+ * @return {@code true} if the call is an external call, {@code false} otherwise.
+ */
+ public boolean isPullableExternalCall() {
+ return VERSION.SDK_INT >= VERSION_CODES.N
+ && (mTelecomCall.getDetails().getCallCapabilities()
+ & CallCompat.Details.CAPABILITY_CAN_PULL_CALL)
+ == CallCompat.Details.CAPABILITY_CAN_PULL_CALL;
+ }
+
+ /**
+ * Determines if answering this call will cause an ongoing video call to be dropped.
+ *
+ * @return {@code true} if answering this call will drop an ongoing video call, {@code false}
+ * otherwise.
+ */
+ public boolean answeringDisconnectsForegroundVideoCall() {
+ Bundle extras = getExtras();
+ if (extras == null
+ || !extras.containsKey(CallCompat.Details.EXTRA_ANSWERING_DROPS_FOREGROUND_CALL)) {
+ return false;
+ }
+ return extras.getBoolean(CallCompat.Details.EXTRA_ANSWERING_DROPS_FOREGROUND_CALL);
+ }
+
+ private void parseCallSpecificAppData() {
+ if (isExternalCall()) {
+ return;
+ }
+
+ mLogState.callSpecificAppData = CallIntentParser.getCallSpecificAppData(getIntentExtras());
+ if (mLogState.callSpecificAppData == null) {
+ mLogState.callSpecificAppData = new CallSpecificAppData();
+ mLogState.callSpecificAppData.callInitiationType =
+ CallInitiationType.Type.EXTERNAL_INITIATION;
+ }
+ if (getState() == State.INCOMING) {
+ mLogState.callSpecificAppData.callInitiationType =
+ CallInitiationType.Type.INCOMING_INITIATION;
+ }
+ }
+
+ @Override
+ public String toString() {
+ if (mTelecomCall == null) {
+ // This should happen only in testing since otherwise we would never have a null
+ // Telecom call.
+ return String.valueOf(mId);
+ }
+
+ return String.format(
+ Locale.US,
+ "[%s, %s, %s, %s, children:%s, parent:%s, "
+ + "conferenceable:%s, videoState:%s, mSessionModificationState:%d, VideoSettings:%s]",
+ mId,
+ State.toString(getState()),
+ Details.capabilitiesToString(mTelecomCall.getDetails().getCallCapabilities()),
+ Details.propertiesToString(mTelecomCall.getDetails().getCallProperties()),
+ mChildCallIds,
+ getParentId(),
+ this.mTelecomCall.getConferenceableCalls(),
+ VideoProfile.videoStateToString(mTelecomCall.getDetails().getVideoState()),
+ mSessionModificationState,
+ getVideoSettings());
+ }
+
+ public String toSimpleString() {
+ return super.toString();
+ }
+
+ @CallHistoryStatus
+ public int getCallHistoryStatus() {
+ return mCallHistoryStatus;
+ }
+
+ public void setCallHistoryStatus(@CallHistoryStatus int callHistoryStatus) {
+ mCallHistoryStatus = callHistoryStatus;
+ }
+
+ public boolean didShowCameraPermission() {
+ return didShowCameraPermission;
+ }
+
+ public void setDidShowCameraPermission(boolean didShow) {
+ didShowCameraPermission = didShow;
+ }
+
+ public boolean isInGlobalSpamList() {
+ return isInGlobalSpamList;
+ }
+
+ public void setIsInGlobalSpamList(boolean inSpamList) {
+ isInGlobalSpamList = inSpamList;
+ }
+
+ public boolean isInUserSpamList() {
+ return isInUserSpamList;
+ }
+
+ public void setIsInUserSpamList(boolean inSpamList) {
+ isInUserSpamList = inSpamList;
+ }
+
+ public boolean isInUserWhiteList() {
+ return isInUserWhiteList;
+ }
+
+ public void setIsInUserWhiteList(boolean inWhiteList) {
+ isInUserWhiteList = inWhiteList;
+ }
+
+ public boolean isSpam() {
+ return mIsSpam;
+ }
+
+ public void setSpam(boolean isSpam) {
+ mIsSpam = isSpam;
+ }
+
+ public boolean isBlocked() {
+ return mIsBlocked;
+ }
+
+ public void setBlockedStatus(boolean isBlocked) {
+ mIsBlocked = isBlocked;
+ }
+
+ public boolean isRemotelyHeld() {
+ return isRemotelyHeld;
+ }
+
+ public boolean isIncoming() {
+ return mLogState.isIncoming;
+ }
+
+ public LatencyReport getLatencyReport() {
+ return mLatencyReport;
+ }
+
+ public void unregisterCallback() {
+ mTelecomCall.unregisterCallback(mTelecomCallCallback);
+ }
+
+ public void acceptUpgradeRequest(int videoState) {
+ LogUtil.i("DialerCall.acceptUpgradeRequest", "videoState: " + videoState);
+ VideoProfile videoProfile = new VideoProfile(videoState);
+ getVideoCall().sendSessionModifyResponse(videoProfile);
+ setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+
+ public void declineUpgradeRequest() {
+ LogUtil.i("DialerCall.declineUpgradeRequest", "");
+ VideoProfile videoProfile = new VideoProfile(getVideoState());
+ getVideoCall().sendSessionModifyResponse(videoProfile);
+ setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+
+ public void phoneAccountSelected(PhoneAccountHandle accountHandle, boolean setDefault) {
+ LogUtil.i(
+ "DialerCall.phoneAccountSelected",
+ "accountHandle: %s, setDefault: %b",
+ accountHandle,
+ setDefault);
+ mTelecomCall.phoneAccountSelected(accountHandle, setDefault);
+ }
+
+ public void disconnect() {
+ LogUtil.i("DialerCall.disconnect", "");
+ setState(DialerCall.State.DISCONNECTING);
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallUpdate();
+ }
+ mTelecomCall.disconnect();
+ }
+
+ public void hold() {
+ LogUtil.i("DialerCall.hold", "");
+ mTelecomCall.hold();
+ }
+
+ public void unhold() {
+ LogUtil.i("DialerCall.unhold", "");
+ mTelecomCall.unhold();
+ }
+
+ public void splitFromConference() {
+ LogUtil.i("DialerCall.splitFromConference", "");
+ mTelecomCall.splitFromConference();
+ }
+
+ public void answer(int videoState) {
+ LogUtil.i("DialerCall.answer", "videoState: " + videoState);
+ mTelecomCall.answer(videoState);
+ }
+
+ public void reject(boolean rejectWithMessage, String message) {
+ LogUtil.i("DialerCall.reject", "");
+ mTelecomCall.reject(rejectWithMessage, message);
+ }
+
+ /** Return the string label to represent the call provider */
+ public String getCallProviderLabel() {
+ if (callProviderLabel == null) {
+ PhoneAccount account = getPhoneAccount();
+ if (account != null && !TextUtils.isEmpty(account.getLabel())) {
+ List<PhoneAccountHandle> accounts =
+ mContext.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts();
+ if (accounts != null && accounts.size() > 1) {
+ callProviderLabel = account.getLabel().toString();
+ }
+ }
+ if (callProviderLabel == null) {
+ callProviderLabel = "";
+ }
+ }
+ return callProviderLabel;
+ }
+
+ private PhoneAccount getPhoneAccount() {
+ PhoneAccountHandle accountHandle = getAccountHandle();
+ if (accountHandle == null) {
+ return null;
+ }
+ return mContext.getSystemService(TelecomManager.class).getPhoneAccount(accountHandle);
+ }
+
+ public String getCallbackNumber() {
+ if (callbackNumber == null) {
+ // Show the emergency callback number if either:
+ // 1. This is an emergency call.
+ // 2. The phone is in Emergency Callback Mode, which means we should show the callback
+ // number.
+ boolean showCallbackNumber = hasProperty(Details.PROPERTY_EMERGENCY_CALLBACK_MODE);
+
+ if (isEmergencyCall() || showCallbackNumber) {
+ callbackNumber = getSubscriptionNumber();
+ } else {
+ StatusHints statusHints = getTelecomCall().getDetails().getStatusHints();
+ if (statusHints != null) {
+ Bundle extras = statusHints.getExtras();
+ if (extras != null) {
+ callbackNumber = extras.getString(TelecomManager.EXTRA_CALL_BACK_NUMBER);
+ }
+ }
+ }
+
+ String simNumber =
+ mContext.getSystemService(TelecomManager.class).getLine1Number(getAccountHandle());
+ if (!showCallbackNumber && PhoneNumberUtils.compare(callbackNumber, simNumber)) {
+ LogUtil.v(
+ "DialerCall.getCallbackNumber",
+ "numbers are the same (and callback number is not being forced to show);"
+ + " not showing the callback number");
+ callbackNumber = "";
+ }
+ if (callbackNumber == null) {
+ callbackNumber = "";
+ }
+ }
+ return callbackNumber;
+ }
+
+ private String getSubscriptionNumber() {
+ // If it's an emergency call, and they're not populating the callback number,
+ // then try to fall back to the phone sub info (to hopefully get the SIM's
+ // number directly from the telephony layer).
+ PhoneAccountHandle accountHandle = getAccountHandle();
+ if (accountHandle != null) {
+ PhoneAccount account =
+ mContext.getSystemService(TelecomManager.class).getPhoneAccount(accountHandle);
+ if (account != null) {
+ return getNumberFromHandle(account.getSubscriptionAddress());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Specifies whether a number is in the call history or not. {@link #CALL_HISTORY_STATUS_UNKNOWN}
+ * means there is no result.
+ */
+ @IntDef({
+ CALL_HISTORY_STATUS_UNKNOWN,
+ CALL_HISTORY_STATUS_PRESENT,
+ CALL_HISTORY_STATUS_NOT_PRESENT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CallHistoryStatus {}
+
+ /* Defines different states of this call */
+ public static class State {
+
+ public static final int INVALID = 0;
+ public static final int NEW = 1; /* The call is new. */
+ public static final int IDLE = 2; /* The call is idle. Nothing active */
+ public static final int ACTIVE = 3; /* There is an active call */
+ public static final int INCOMING = 4; /* A normal incoming phone call */
+ public static final int CALL_WAITING = 5; /* Incoming call while another is active */
+ public static final int DIALING = 6; /* An outgoing call during dial phase */
+ public static final int REDIALING = 7; /* Subsequent dialing attempt after a failure */
+ public static final int ONHOLD = 8; /* An active phone call placed on hold */
+ public static final int DISCONNECTING = 9; /* A call is being ended. */
+ public static final int DISCONNECTED = 10; /* State after a call disconnects */
+ public static final int CONFERENCED = 11; /* DialerCall part of a conference call */
+ public static final int SELECT_PHONE_ACCOUNT = 12; /* Waiting for account selection */
+ public static final int CONNECTING = 13; /* Waiting for Telecom broadcast to finish */
+ public static final int BLOCKED = 14; /* The number was found on the block list */
+ public static final int PULLING = 15; /* An external call being pulled to the device */
+
+ public static boolean isConnectingOrConnected(int state) {
+ switch (state) {
+ case ACTIVE:
+ case INCOMING:
+ case CALL_WAITING:
+ case CONNECTING:
+ case DIALING:
+ case PULLING:
+ case REDIALING:
+ case ONHOLD:
+ case CONFERENCED:
+ return true;
+ default:
+ }
+ return false;
+ }
+
+ public static boolean isDialing(int state) {
+ return state == DIALING || state == PULLING || state == REDIALING;
+ }
+
+ public static String toString(int state) {
+ switch (state) {
+ case INVALID:
+ return "INVALID";
+ case NEW:
+ return "NEW";
+ case IDLE:
+ return "IDLE";
+ case ACTIVE:
+ return "ACTIVE";
+ case INCOMING:
+ return "INCOMING";
+ case CALL_WAITING:
+ return "CALL_WAITING";
+ case DIALING:
+ return "DIALING";
+ case PULLING:
+ return "PULLING";
+ case REDIALING:
+ return "REDIALING";
+ case ONHOLD:
+ return "ONHOLD";
+ case DISCONNECTING:
+ return "DISCONNECTING";
+ case DISCONNECTED:
+ return "DISCONNECTED";
+ case CONFERENCED:
+ return "CONFERENCED";
+ case SELECT_PHONE_ACCOUNT:
+ return "SELECT_PHONE_ACCOUNT";
+ case CONNECTING:
+ return "CONNECTING";
+ case BLOCKED:
+ return "BLOCKED";
+ default:
+ return "UNKNOWN";
+ }
+ }
+ }
+
+ /**
+ * Defines different states of session modify requests, which are used to upgrade to video, or
+ * downgrade to audio.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ SESSION_MODIFICATION_STATE_NO_REQUEST,
+ SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE,
+ SESSION_MODIFICATION_STATE_REQUEST_FAILED,
+ SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
+ SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT,
+ SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED,
+ SESSION_MODIFICATION_STATE_REQUEST_REJECTED,
+ SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE
+ })
+ public @interface SessionModificationState {}
+
+ public static final int SESSION_MODIFICATION_STATE_NO_REQUEST = 0;
+ public static final int SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE = 1;
+ public static final int SESSION_MODIFICATION_STATE_REQUEST_FAILED = 2;
+ public static final int SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST = 3;
+ public static final int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT = 4;
+ public static final int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED = 5;
+ public static final int SESSION_MODIFICATION_STATE_REQUEST_REJECTED = 6;
+ public static final int SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE = 7;
+
+ public static class VideoSettings {
+
+ public static final int CAMERA_DIRECTION_UNKNOWN = -1;
+ public static final int CAMERA_DIRECTION_FRONT_FACING = CameraCharacteristics.LENS_FACING_FRONT;
+ public static final int CAMERA_DIRECTION_BACK_FACING = CameraCharacteristics.LENS_FACING_BACK;
+
+ private int mCameraDirection = CAMERA_DIRECTION_UNKNOWN;
+
+ /**
+ * Gets the camera direction. if camera direction is set to CAMERA_DIRECTION_UNKNOWN, the video
+ * state of the call should be used to infer the camera direction.
+ *
+ * @see {@link CameraCharacteristics#LENS_FACING_FRONT}
+ * @see {@link CameraCharacteristics#LENS_FACING_BACK}
+ */
+ public int getCameraDir() {
+ return mCameraDirection;
+ }
+
+ /**
+ * Sets the camera direction. if camera direction is set to CAMERA_DIRECTION_UNKNOWN, the video
+ * state of the call should be used to infer the camera direction.
+ *
+ * @see {@link CameraCharacteristics#LENS_FACING_FRONT}
+ * @see {@link CameraCharacteristics#LENS_FACING_BACK}
+ */
+ public void setCameraDir(int cameraDirection) {
+ if (cameraDirection == CAMERA_DIRECTION_FRONT_FACING
+ || cameraDirection == CAMERA_DIRECTION_BACK_FACING) {
+ mCameraDirection = cameraDirection;
+ } else {
+ mCameraDirection = CAMERA_DIRECTION_UNKNOWN;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "(CameraDir:" + getCameraDir() + ")";
+ }
+ }
+
+ /**
+ * Tracks any state variables that is useful for logging. There is some amount of overlap with
+ * existing call member variables, but this duplication helps to ensure that none of these logging
+ * variables will interface with/and affect call logic.
+ */
+ public static class LogState {
+
+ public DisconnectCause disconnectCause;
+ public boolean isIncoming = false;
+ public int contactLookupResult = ContactLookupResult.Type.UNKNOWN_LOOKUP_RESULT_TYPE;
+ public CallSpecificAppData callSpecificAppData;
+ // If this was a conference call, the total number of calls involved in the conference.
+ public int conferencedCalls = 0;
+ public long duration = 0;
+ public boolean isLogged = false;
+
+ private static String lookupToString(int lookupType) {
+ switch (lookupType) {
+ case ContactLookupResult.Type.LOCAL_CONTACT:
+ return "Local";
+ case ContactLookupResult.Type.LOCAL_CACHE:
+ return "Cache";
+ case ContactLookupResult.Type.REMOTE:
+ return "Remote";
+ case ContactLookupResult.Type.EMERGENCY:
+ return "Emergency";
+ case ContactLookupResult.Type.VOICEMAIL:
+ return "Voicemail";
+ default:
+ return "Not found";
+ }
+ }
+
+ private static String initiationToString(CallSpecificAppData callSpecificAppData) {
+ if (callSpecificAppData == null) {
+ return "null";
+ }
+ switch (callSpecificAppData.callInitiationType) {
+ case CallInitiationType.Type.INCOMING_INITIATION:
+ return "Incoming";
+ case CallInitiationType.Type.DIALPAD:
+ return "Dialpad";
+ case CallInitiationType.Type.SPEED_DIAL:
+ return "Speed Dial";
+ case CallInitiationType.Type.REMOTE_DIRECTORY:
+ return "Remote Directory";
+ case CallInitiationType.Type.SMART_DIAL:
+ return "Smart Dial";
+ case CallInitiationType.Type.REGULAR_SEARCH:
+ return "Regular Search";
+ case CallInitiationType.Type.CALL_LOG:
+ return "DialerCall Log";
+ case CallInitiationType.Type.CALL_LOG_FILTER:
+ return "DialerCall Log Filter";
+ case CallInitiationType.Type.VOICEMAIL_LOG:
+ return "Voicemail Log";
+ case CallInitiationType.Type.CALL_DETAILS:
+ return "DialerCall Details";
+ case CallInitiationType.Type.QUICK_CONTACTS:
+ return "Quick Contacts";
+ case CallInitiationType.Type.EXTERNAL_INITIATION:
+ return "External";
+ case CallInitiationType.Type.LAUNCHER_SHORTCUT:
+ return "Launcher Shortcut";
+ default:
+ return "Unknown: " + callSpecificAppData.callInitiationType;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US,
+ "["
+ + "%s, " // DisconnectCause toString already describes the object type
+ + "isIncoming: %s, "
+ + "contactLookup: %s, "
+ + "callInitiation: %s, "
+ + "duration: %s"
+ + "]",
+ disconnectCause,
+ isIncoming,
+ lookupToString(contactLookupResult),
+ initiationToString(callSpecificAppData),
+ duration);
+ }
+ }
+
+ /** Called when canned text responses have been loaded. */
+ public interface CannedTextResponsesLoadedListener {
+ void onCannedTextResponsesLoaded(DialerCall call);
+ }
+}
diff --git a/java/com/android/incallui/call/DialerCallDelegate.java b/java/com/android/incallui/call/DialerCallDelegate.java
new file mode 100644
index 000000000..463b4916a
--- /dev/null
+++ b/java/com/android/incallui/call/DialerCallDelegate.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.call;
+
+import android.telecom.Call;
+
+/** Callback from the call module to the container. */
+public interface DialerCallDelegate {
+
+ DialerCall getDialerCallFromTelecomCall(Call telecomCall);
+}
diff --git a/java/com/android/incallui/call/DialerCallListener.java b/java/com/android/incallui/call/DialerCallListener.java
new file mode 100644
index 000000000..b426cd72e
--- /dev/null
+++ b/java/com/android/incallui/call/DialerCallListener.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.call;
+
+import com.android.incallui.call.DialerCall.SessionModificationState;
+
+/** Used to monitor state changes in a dialer call. */
+public interface DialerCallListener {
+
+ void onDialerCallDisconnect();
+
+ void onDialerCallUpdate();
+
+ void onDialerCallChildNumberChange();
+
+ void onDialerCallLastForwardedNumberChange();
+
+ void onDialerCallUpgradeToVideo();
+
+ void onDialerCallSessionModificationStateChange(@SessionModificationState int state);
+
+ void onWiFiToLteHandover();
+
+ void onHandoverToWifiFailure();
+}
diff --git a/java/com/android/incallui/call/ExternalCallList.java b/java/com/android/incallui/call/ExternalCallList.java
new file mode 100644
index 000000000..52a7a304b
--- /dev/null
+++ b/java/com/android/incallui/call/ExternalCallList.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.call;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.telecom.Call;
+import android.util.ArraySet;
+import com.android.contacts.common.compat.CallCompat;
+import com.android.dialer.common.LogUtil;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Tracks the external calls known to the InCall UI.
+ *
+ * <p>External calls are those with {@code android.telecom.Call.Details#PROPERTY_IS_EXTERNAL_CALL}.
+ */
+public class ExternalCallList {
+
+ private final Set<Call> mExternalCalls = new ArraySet<>();
+ private final Set<ExternalCallListener> mExternalCallListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<ExternalCallListener, Boolean>(8, 0.9f, 1));
+ /** Handles {@link android.telecom.Call.Callback} callbacks. */
+ private final Call.Callback mTelecomCallCallback =
+ new Call.Callback() {
+ @Override
+ public void onDetailsChanged(Call call, Call.Details details) {
+ notifyExternalCallUpdated(call);
+ }
+ };
+
+ /** Begins tracking an external call and notifies listeners of the new call. */
+ public void onCallAdded(Call telecomCall) {
+ if (!telecomCall.getDetails().hasProperty(CallCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) {
+ throw new IllegalArgumentException();
+ }
+ mExternalCalls.add(telecomCall);
+ telecomCall.registerCallback(mTelecomCallCallback, new Handler(Looper.getMainLooper()));
+ notifyExternalCallAdded(telecomCall);
+ }
+
+ /** Stops tracking an external call and notifies listeners of the removal of the call. */
+ public void onCallRemoved(Call telecomCall) {
+ if (!mExternalCalls.contains(telecomCall)) {
+ // This can happen on M for external calls from blocked numbers
+ LogUtil.i("ExternalCallList.onCallRemoved", "attempted to remove unregistered call");
+ return;
+ }
+ mExternalCalls.remove(telecomCall);
+ telecomCall.unregisterCallback(mTelecomCallCallback);
+ notifyExternalCallRemoved(telecomCall);
+ }
+
+ /** Adds a new listener to external call events. */
+ public void addExternalCallListener(@NonNull ExternalCallListener listener) {
+ mExternalCallListeners.add(listener);
+ }
+
+ /** Removes a listener to external call events. */
+ public void removeExternalCallListener(@NonNull ExternalCallListener listener) {
+ if (!mExternalCallListeners.contains(listener)) {
+ LogUtil.i(
+ "ExternalCallList.removeExternalCallListener",
+ "attempt to remove unregistered listener.");
+ }
+ mExternalCallListeners.remove(listener);
+ }
+
+ public boolean isCallTracked(@NonNull android.telecom.Call telecomCall) {
+ return mExternalCalls.contains(telecomCall);
+ }
+
+ /** Notifies listeners of the addition of a new external call. */
+ private void notifyExternalCallAdded(Call call) {
+ for (ExternalCallListener listener : mExternalCallListeners) {
+ listener.onExternalCallAdded(call);
+ }
+ }
+
+ /** Notifies listeners of the removal of an external call. */
+ private void notifyExternalCallRemoved(Call call) {
+ for (ExternalCallListener listener : mExternalCallListeners) {
+ listener.onExternalCallRemoved(call);
+ }
+ }
+
+ /** Notifies listeners of changes to an external call. */
+ private void notifyExternalCallUpdated(Call call) {
+ if (!call.getDetails().hasProperty(CallCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) {
+ // A previous external call has been pulled and is now a regular call, so we will remove
+ // it from the external call listener and ensure that the CallList is informed of the
+ // change.
+ onCallRemoved(call);
+
+ for (ExternalCallListener listener : mExternalCallListeners) {
+ listener.onExternalCallPulled(call);
+ }
+ } else {
+ for (ExternalCallListener listener : mExternalCallListeners) {
+ listener.onExternalCallUpdated(call);
+ }
+ }
+ }
+
+ /**
+ * Defines events which the {@link ExternalCallList} exposes to interested components (e.g. {@link
+ * com.android.incallui.ExternalCallNotifier ExternalCallNotifier}).
+ */
+ public interface ExternalCallListener {
+
+ void onExternalCallAdded(Call call);
+
+ void onExternalCallRemoved(Call call);
+
+ void onExternalCallUpdated(Call call);
+
+ void onExternalCallPulled(Call call);
+ }
+}
diff --git a/java/com/android/incallui/call/InCallServiceListener.java b/java/com/android/incallui/call/InCallServiceListener.java
new file mode 100644
index 000000000..e48ce9d79
--- /dev/null
+++ b/java/com/android/incallui/call/InCallServiceListener.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.call;
+
+import android.telecom.InCallService;
+
+/**
+ * Interface implemented by In-Call components that maintain a reference to the Telecom API {@code
+ * InCallService} object. Clarifies the expectations associated with the relevant method calls.
+ */
+public interface InCallServiceListener {
+
+ /**
+ * Called once at {@code InCallService} startup time with a valid instance. At that time, there
+ * will be no existing {@code DialerCall}s.
+ *
+ * @param inCallService The {@code InCallService} object.
+ */
+ void setInCallService(InCallService inCallService);
+
+ /**
+ * Called once at {@code InCallService} shutdown time. At that time, any {@code DialerCall}s will
+ * have transitioned through the disconnected state and will no longer exist.
+ */
+ void clearInCallService();
+}
diff --git a/java/com/android/incallui/call/InCallUiLegacyBindings.java b/java/com/android/incallui/call/InCallUiLegacyBindings.java
new file mode 100644
index 000000000..1b0ed4542
--- /dev/null
+++ b/java/com/android/incallui/call/InCallUiLegacyBindings.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.call;
+
+/**
+ * These are old bindings between InCallUi and the container application. All new bindings should be
+ * added to the bindings module and not here.
+ */
+public interface InCallUiLegacyBindings {
+
+ void logCall(DialerCall call);
+}
diff --git a/java/com/android/incallui/call/InCallUiLegacyBindingsFactory.java b/java/com/android/incallui/call/InCallUiLegacyBindingsFactory.java
new file mode 100644
index 000000000..8604976f7
--- /dev/null
+++ b/java/com/android/incallui/call/InCallUiLegacyBindingsFactory.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.call;
+
+/**
+ * This interface should be implementated by the Application subclass. It allows the in call UI
+ * module to get references to the InCallUiLegacyBindings.
+ */
+public interface InCallUiLegacyBindingsFactory {
+
+ InCallUiLegacyBindings newInCallUiLegacyBindings();
+}
diff --git a/java/com/android/incallui/call/InCallUiLegacyBindingsStub.java b/java/com/android/incallui/call/InCallUiLegacyBindingsStub.java
new file mode 100644
index 000000000..8869c64b2
--- /dev/null
+++ b/java/com/android/incallui/call/InCallUiLegacyBindingsStub.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.call;
+
+/** Default implementation for in call UI legacy bindings. */
+public class InCallUiLegacyBindingsStub implements InCallUiLegacyBindings {
+
+ @Override
+ public void logCall(DialerCall call) {}
+}
diff --git a/java/com/android/incallui/call/InCallVideoCallCallback.java b/java/com/android/incallui/call/InCallVideoCallCallback.java
new file mode 100644
index 000000000..f897ac9dd
--- /dev/null
+++ b/java/com/android/incallui/call/InCallVideoCallCallback.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.call;
+
+import android.os.Handler;
+import android.support.annotation.Nullable;
+import android.telecom.Connection;
+import android.telecom.Connection.VideoProvider;
+import android.telecom.InCallService.VideoCall;
+import android.telecom.VideoProfile;
+import android.telecom.VideoProfile.CameraCapabilities;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+
+/** Implements the InCallUI VideoCall Callback. */
+public class InCallVideoCallCallback extends VideoCall.Callback implements Runnable {
+
+ private static final int CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS = 4000;
+
+ private final DialerCall call;
+ @Nullable private Handler handler;
+ @SessionModificationState private int newSessionModificationState;
+
+ public InCallVideoCallCallback(DialerCall call) {
+ this.call = call;
+ }
+
+ @Override
+ public void onSessionModifyRequestReceived(VideoProfile videoProfile) {
+ LogUtil.i(
+ "InCallVideoCallCallback.onSessionModifyRequestReceived", "videoProfile: " + videoProfile);
+ int previousVideoState = VideoUtils.getUnPausedVideoState(call.getVideoState());
+ int newVideoState = VideoUtils.getUnPausedVideoState(videoProfile.getVideoState());
+
+ boolean wasVideoCall = VideoUtils.isVideoCall(previousVideoState);
+ boolean isVideoCall = VideoUtils.isVideoCall(newVideoState);
+
+ if (wasVideoCall && !isVideoCall) {
+ LogUtil.v(
+ "InCallVideoCallCallback.onSessionModifyRequestReceived",
+ "call downgraded to " + newVideoState);
+ } else if (previousVideoState != newVideoState) {
+ InCallVideoCallCallbackNotifier.getInstance().upgradeToVideoRequest(call, newVideoState);
+ }
+ }
+
+ /**
+ * @param status Status of the session modify request. Valid values are {@link
+ * Connection.VideoProvider#SESSION_MODIFY_REQUEST_SUCCESS}, {@link
+ * Connection.VideoProvider#SESSION_MODIFY_REQUEST_FAIL}, {@link
+ * Connection.VideoProvider#SESSION_MODIFY_REQUEST_INVALID}
+ * @param responseProfile The actual profile changes made by the peer device.
+ */
+ @Override
+ public void onSessionModifyResponseReceived(
+ int status, VideoProfile requestedProfile, VideoProfile responseProfile) {
+ LogUtil.i(
+ "InCallVideoCallCallback.onSessionModifyResponseReceived",
+ "status: %d, "
+ + "requestedProfile: %s, responseProfile: %s, current session modification state: %d",
+ status,
+ requestedProfile,
+ responseProfile,
+ call.getSessionModificationState());
+
+ if (call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE) {
+ if (handler == null) {
+ handler = new Handler();
+ } else {
+ handler.removeCallbacks(this);
+ }
+
+ newSessionModificationState = getDialerSessionModifyStateTelecomStatus(status);
+ if (status != VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) {
+ // This will update the video UI to display the error message.
+ call.setSessionModificationState(newSessionModificationState);
+ }
+
+ // Wait for 4 seconds and then clean the session modification state. This allows the video UI
+ // to stay up so that the user can read the error message.
+ //
+ // If the other person accepted the upgrade request then this will keep the video UI up until
+ // the call's video state change. Without this we would switch to the voice call and then
+ // switch back to video UI.
+ handler.postDelayed(this, CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS);
+ } else if (call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ call.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ } else if (call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE) {
+ call.setSessionModificationState(getDialerSessionModifyStateTelecomStatus(status));
+ } else {
+ LogUtil.i(
+ "InCallVideoCallCallback.onSessionModifyResponseReceived",
+ "call is not waiting for " + "response, doing nothing");
+ }
+ }
+
+ @SessionModificationState
+ private int getDialerSessionModifyStateTelecomStatus(int telecomStatus) {
+ switch (telecomStatus) {
+ case VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS:
+ return DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST;
+ case VideoProvider.SESSION_MODIFY_REQUEST_FAIL:
+ case VideoProvider.SESSION_MODIFY_REQUEST_INVALID:
+ // Check if it's already video call, which means the request is not video upgrade request.
+ if (VideoUtils.isVideoCall(call.getVideoState())) {
+ return DialerCall.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
+ } else {
+ return DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED;
+ }
+ case VideoProvider.SESSION_MODIFY_REQUEST_TIMED_OUT:
+ return DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
+ case VideoProvider.SESSION_MODIFY_REQUEST_REJECTED_BY_REMOTE:
+ return DialerCall.SESSION_MODIFICATION_STATE_REQUEST_REJECTED;
+ default:
+ LogUtil.e(
+ "InCallVideoCallCallback.getDialerSessionModifyStateTelecomStatus",
+ "unknown status: %d",
+ telecomStatus);
+ return DialerCall.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
+ }
+ }
+
+ @Override
+ public void onCallSessionEvent(int event) {
+ InCallVideoCallCallbackNotifier.getInstance().callSessionEvent(event);
+ }
+
+ @Override
+ public void onPeerDimensionsChanged(int width, int height) {
+ InCallVideoCallCallbackNotifier.getInstance().peerDimensionsChanged(call, width, height);
+ }
+
+ @Override
+ public void onVideoQualityChanged(int videoQuality) {
+ InCallVideoCallCallbackNotifier.getInstance().videoQualityChanged(call, videoQuality);
+ }
+
+ /**
+ * Handles a change to the call data usage. No implementation as the in-call UI does not display
+ * data usage.
+ *
+ * @param dataUsage The updated data usage.
+ */
+ @Override
+ public void onCallDataUsageChanged(long dataUsage) {
+ LogUtil.v("InCallVideoCallCallback.onCallDataUsageChanged", "dataUsage = " + dataUsage);
+ InCallVideoCallCallbackNotifier.getInstance().callDataUsageChanged(dataUsage);
+ }
+
+ /**
+ * Handles changes to the camera capabilities. No implementation as the in-call UI does not make
+ * use of camera capabilities.
+ *
+ * @param cameraCapabilities The changed camera capabilities.
+ */
+ @Override
+ public void onCameraCapabilitiesChanged(CameraCapabilities cameraCapabilities) {
+ if (cameraCapabilities != null) {
+ InCallVideoCallCallbackNotifier.getInstance()
+ .cameraDimensionsChanged(
+ call, cameraCapabilities.getWidth(), cameraCapabilities.getHeight());
+ }
+ }
+
+ /**
+ * Called 4 seconds after the remote user responds to the video upgrade request. We use this to
+ * clear the session modify state.
+ */
+ @Override
+ public void run() {
+ if (call.getSessionModificationState() == newSessionModificationState) {
+ LogUtil.i("InCallVideoCallCallback.onSessionModifyResponseReceived", "clearing state");
+ call.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ } else {
+ LogUtil.i(
+ "InCallVideoCallCallback.onSessionModifyResponseReceived",
+ "session modification state has changed, not clearing state");
+ }
+ }
+}
diff --git a/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java b/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java
new file mode 100644
index 000000000..4a949263c
--- /dev/null
+++ b/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.call;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.LogUtil;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Class used by {@link InCallService.VideoCallCallback} to notify interested parties of incoming
+ * events.
+ */
+public class InCallVideoCallCallbackNotifier {
+
+ /** Singleton instance of this class. */
+ private static InCallVideoCallCallbackNotifier sInstance = new InCallVideoCallCallbackNotifier();
+
+ /**
+ * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is load factor before
+ * resizing, 1 means we only expect a single thread to access the map so make only a single shard
+ */
+ private final Set<SessionModificationListener> mSessionModificationListeners =
+ Collections.newSetFromMap(
+ new ConcurrentHashMap<SessionModificationListener, Boolean>(8, 0.9f, 1));
+
+ private final Set<VideoEventListener> mVideoEventListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<VideoEventListener, Boolean>(8, 0.9f, 1));
+ private final Set<SurfaceChangeListener> mSurfaceChangeListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<SurfaceChangeListener, Boolean>(8, 0.9f, 1));
+
+ /** Private constructor. Instance should only be acquired through getInstance(). */
+ private InCallVideoCallCallbackNotifier() {}
+
+ /** Static singleton accessor method. */
+ public static InCallVideoCallCallbackNotifier getInstance() {
+ return sInstance;
+ }
+
+ /**
+ * Adds a new {@link SessionModificationListener}.
+ *
+ * @param listener The listener.
+ */
+ public void addSessionModificationListener(@NonNull SessionModificationListener listener) {
+ Objects.requireNonNull(listener);
+ mSessionModificationListeners.add(listener);
+ }
+
+ /**
+ * Remove a {@link SessionModificationListener}.
+ *
+ * @param listener The listener.
+ */
+ public void removeSessionModificationListener(@Nullable SessionModificationListener listener) {
+ if (listener != null) {
+ mSessionModificationListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Adds a new {@link VideoEventListener}.
+ *
+ * @param listener The listener.
+ */
+ public void addVideoEventListener(@NonNull VideoEventListener listener) {
+ Objects.requireNonNull(listener);
+ mVideoEventListeners.add(listener);
+ }
+
+ /**
+ * Remove a {@link VideoEventListener}.
+ *
+ * @param listener The listener.
+ */
+ public void removeVideoEventListener(@Nullable VideoEventListener listener) {
+ if (listener != null) {
+ mVideoEventListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Adds a new {@link SurfaceChangeListener}.
+ *
+ * @param listener The listener.
+ */
+ public void addSurfaceChangeListener(@NonNull SurfaceChangeListener listener) {
+ Objects.requireNonNull(listener);
+ mSurfaceChangeListeners.add(listener);
+ }
+
+ /**
+ * Remove a {@link SurfaceChangeListener}.
+ *
+ * @param listener The listener.
+ */
+ public void removeSurfaceChangeListener(@Nullable SurfaceChangeListener listener) {
+ if (listener != null) {
+ mSurfaceChangeListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Inform listeners of an upgrade to video request for a call.
+ *
+ * @param call The call.
+ * @param videoState The video state we want to upgrade to.
+ */
+ public void upgradeToVideoRequest(DialerCall call, int videoState) {
+ LogUtil.v(
+ "InCallVideoCallCallbackNotifier.upgradeToVideoRequest",
+ "call = " + call + " new video state = " + videoState);
+ for (SessionModificationListener listener : mSessionModificationListeners) {
+ listener.onUpgradeToVideoRequest(call, videoState);
+ }
+ }
+
+ /**
+ * Inform listeners of a call session event.
+ *
+ * @param event The call session event.
+ */
+ public void callSessionEvent(int event) {
+ for (VideoEventListener listener : mVideoEventListeners) {
+ listener.onCallSessionEvent(event);
+ }
+ }
+
+ /**
+ * Inform listeners of a downgrade to audio.
+ *
+ * @param call The call.
+ * @param paused The paused state.
+ */
+ public void peerPausedStateChanged(DialerCall call, boolean paused) {
+ for (VideoEventListener listener : mVideoEventListeners) {
+ listener.onPeerPauseStateChanged(call, paused);
+ }
+ }
+
+ /**
+ * Inform listeners of any change in the video quality of the call
+ *
+ * @param call The call.
+ * @param videoQuality The updated video quality of the call.
+ */
+ public void videoQualityChanged(DialerCall call, int videoQuality) {
+ for (VideoEventListener listener : mVideoEventListeners) {
+ listener.onVideoQualityChanged(call, videoQuality);
+ }
+ }
+
+ /**
+ * Inform listeners of a change to peer dimensions.
+ *
+ * @param call The call.
+ * @param width New peer width.
+ * @param height New peer height.
+ */
+ public void peerDimensionsChanged(DialerCall call, int width, int height) {
+ for (SurfaceChangeListener listener : mSurfaceChangeListeners) {
+ listener.onUpdatePeerDimensions(call, width, height);
+ }
+ }
+
+ /**
+ * Inform listeners of a change to camera dimensions.
+ *
+ * @param call The call.
+ * @param width The new camera video width.
+ * @param height The new camera video height.
+ */
+ public void cameraDimensionsChanged(DialerCall call, int width, int height) {
+ for (SurfaceChangeListener listener : mSurfaceChangeListeners) {
+ listener.onCameraDimensionsChange(call, width, height);
+ }
+ }
+
+ /**
+ * Inform listeners of a change to call data usage.
+ *
+ * @param dataUsage data usage value
+ */
+ public void callDataUsageChanged(long dataUsage) {
+ for (VideoEventListener listener : mVideoEventListeners) {
+ listener.onCallDataUsageChange(dataUsage);
+ }
+ }
+
+ /** Listener interface for any class that wants to be notified of upgrade to video request. */
+ public interface SessionModificationListener {
+
+ /**
+ * Called when a peer request is received to upgrade an audio-only call to a video call.
+ *
+ * @param call The call the request was received for.
+ * @param videoState The requested video state.
+ */
+ void onUpgradeToVideoRequest(DialerCall call, int videoState);
+ }
+
+ /**
+ * Listener interface for any class that wants to be notified of video events, including pause and
+ * un-pause of peer video, video quality changes.
+ */
+ public interface VideoEventListener {
+
+ /**
+ * Called when the peer pauses or un-pauses video transmission.
+ *
+ * @param call The call which paused or un-paused video transmission.
+ * @param paused {@code True} when the video transmission is paused, {@code false} otherwise.
+ */
+ void onPeerPauseStateChanged(DialerCall call, boolean paused);
+
+ /**
+ * Called when the video quality changes.
+ *
+ * @param call The call whose video quality changes.
+ * @param videoCallQuality - values are QUALITY_HIGH, MEDIUM, LOW and UNKNOWN.
+ */
+ void onVideoQualityChanged(DialerCall call, int videoCallQuality);
+
+ /*
+ * Called when call data usage value is requested or when call data usage value is updated
+ * because of a call state change
+ *
+ * @param dataUsage call data usage value
+ */
+ void onCallDataUsageChange(long dataUsage);
+
+ /**
+ * Called when call session event is raised.
+ *
+ * @param event The call session event.
+ */
+ void onCallSessionEvent(int event);
+ }
+
+ /**
+ * Listener interface for any class that wants to be notified of changes to the video surfaces.
+ */
+ public interface SurfaceChangeListener {
+
+ /**
+ * Called when the peer video feed changes dimensions. This can occur when the peer rotates
+ * their device, changing the aspect ratio of the video signal.
+ *
+ * @param call The call which experienced a peer video
+ */
+ void onUpdatePeerDimensions(DialerCall call, int width, int height);
+
+ /**
+ * Called when the local camera changes dimensions. This occurs when a change in camera occurs.
+ *
+ * @param call The call which experienced the camera dimension change.
+ * @param width The new camera video width.
+ * @param height The new camera video height.
+ */
+ void onCameraDimensionsChange(DialerCall call, int width, int height);
+ }
+}
diff --git a/java/com/android/incallui/call/TelecomAdapter.java b/java/com/android/incallui/call/TelecomAdapter.java
new file mode 100644
index 000000000..ebf4ecf4f
--- /dev/null
+++ b/java/com/android/incallui/call/TelecomAdapter.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.call;
+
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.os.Looper;
+import android.support.annotation.MainThread;
+import android.telecom.InCallService;
+import com.android.dialer.common.LogUtil;
+import java.util.List;
+
+/** Wrapper around Telecom APIs. */
+public final class TelecomAdapter implements InCallServiceListener {
+
+ private static final String ADD_CALL_MODE_KEY = "add_call_mode";
+
+ private static TelecomAdapter sInstance;
+ private InCallService mInCallService;
+
+ private TelecomAdapter() {}
+
+ @MainThread
+ public static TelecomAdapter getInstance() {
+ if (!Looper.getMainLooper().isCurrentThread()) {
+ throw new IllegalStateException();
+ }
+ if (sInstance == null) {
+ sInstance = new TelecomAdapter();
+ }
+ return sInstance;
+ }
+
+ @Override
+ public void setInCallService(InCallService inCallService) {
+ mInCallService = inCallService;
+ }
+
+ @Override
+ public void clearInCallService() {
+ mInCallService = null;
+ }
+
+ private android.telecom.Call getTelecomCallById(String callId) {
+ DialerCall call = CallList.getInstance().getCallById(callId);
+ return call == null ? null : call.getTelecomCall();
+ }
+
+ public void mute(boolean shouldMute) {
+ if (mInCallService != null) {
+ mInCallService.setMuted(shouldMute);
+ } else {
+ LogUtil.e("TelecomAdapter.mute", "mInCallService is null");
+ }
+ }
+
+ public void setAudioRoute(int route) {
+ if (mInCallService != null) {
+ mInCallService.setAudioRoute(route);
+ } else {
+ LogUtil.e("TelecomAdapter.setAudioRoute", "mInCallService is null");
+ }
+ }
+
+ public void merge(String callId) {
+ android.telecom.Call call = getTelecomCallById(callId);
+ if (call != null) {
+ List<android.telecom.Call> conferenceable = call.getConferenceableCalls();
+ if (!conferenceable.isEmpty()) {
+ call.conference(conferenceable.get(0));
+ } else {
+ if (call.getDetails().can(android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE)) {
+ call.mergeConference();
+ }
+ }
+ } else {
+ LogUtil.e("TelecomAdapter.merge", "call not in call list " + callId);
+ }
+ }
+
+ public void swap(String callId) {
+ android.telecom.Call call = getTelecomCallById(callId);
+ if (call != null) {
+ if (call.getDetails().can(android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE)) {
+ call.swapConference();
+ }
+ } else {
+ LogUtil.e("TelecomAdapter.swap", "call not in call list " + callId);
+ }
+ }
+
+ public void addCall() {
+ if (mInCallService != null) {
+ Intent intent = new Intent(Intent.ACTION_DIAL);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ // when we request the dialer come up, we also want to inform
+ // it that we're going through the "add call" option from the
+ // InCallScreen / PhoneUtils.
+ intent.putExtra(ADD_CALL_MODE_KEY, true);
+ try {
+ LogUtil.d("TelecomAdapter.addCall", "Sending the add DialerCall intent");
+ mInCallService.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ // This is rather rare but possible.
+ // Note: this method is used even when the phone is encrypted. At that moment
+ // the system may not find any Activity which can accept this Intent.
+ LogUtil.e("TelecomAdapter.addCall", "Activity for adding calls isn't found.", e);
+ }
+ }
+ }
+
+ public void playDtmfTone(String callId, char digit) {
+ android.telecom.Call call = getTelecomCallById(callId);
+ if (call != null) {
+ call.playDtmfTone(digit);
+ } else {
+ LogUtil.e("TelecomAdapter.playDtmfTone", "call not in call list " + callId);
+ }
+ }
+
+ public void stopDtmfTone(String callId) {
+ android.telecom.Call call = getTelecomCallById(callId);
+ if (call != null) {
+ call.stopDtmfTone();
+ } else {
+ LogUtil.e("TelecomAdapter.stopDtmfTone", "call not in call list " + callId);
+ }
+ }
+
+ public void postDialContinue(String callId, boolean proceed) {
+ android.telecom.Call call = getTelecomCallById(callId);
+ if (call != null) {
+ call.postDialContinue(proceed);
+ } else {
+ LogUtil.e("TelecomAdapter.postDialContinue", "call not in call list " + callId);
+ }
+ }
+
+ public boolean canAddCall() {
+ if (mInCallService != null) {
+ return mInCallService.canAddCall();
+ }
+ return false;
+ }
+}
diff --git a/java/com/android/incallui/call/VideoUtils.java b/java/com/android/incallui/call/VideoUtils.java
new file mode 100644
index 000000000..80fbfb1cc
--- /dev/null
+++ b/java/com/android/incallui/call/VideoUtils.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.call;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.content.ContextCompat;
+import android.telecom.VideoProfile;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.util.DialerUtils;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import java.util.Objects;
+
+public class VideoUtils {
+
+ private static final String PREFERENCE_CAMERA_ALLOWED_BY_USER = "camera_allowed_by_user";
+
+ public static boolean isVideoCall(@Nullable DialerCall call) {
+ return call != null && isVideoCall(call.getVideoState());
+ }
+
+ public static boolean isVideoCall(int videoState) {
+ return CompatUtils.isVideoCompatible()
+ && (VideoProfile.isTransmissionEnabled(videoState)
+ || VideoProfile.isReceptionEnabled(videoState));
+ }
+
+ public static boolean hasSentVideoUpgradeRequest(@Nullable DialerCall call) {
+ return call != null && hasSentVideoUpgradeRequest(call.getSessionModificationState());
+ }
+
+ public static boolean hasSentVideoUpgradeRequest(@SessionModificationState int state) {
+ return state == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
+ || state == DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED
+ || state == DialerCall.SESSION_MODIFICATION_STATE_REQUEST_REJECTED
+ || state == DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
+ }
+
+ public static boolean hasReceivedVideoUpgradeRequest(@Nullable DialerCall call) {
+ return call != null && hasReceivedVideoUpgradeRequest(call.getSessionModificationState());
+ }
+
+ public static boolean hasReceivedVideoUpgradeRequest(@SessionModificationState int state) {
+ return state == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
+ }
+
+ public static boolean isBidirectionalVideoCall(DialerCall call) {
+ return CompatUtils.isVideoCompatible() && VideoProfile.isBidirectional(call.getVideoState());
+ }
+
+ public static boolean isTransmissionEnabled(DialerCall call) {
+ if (!CompatUtils.isVideoCompatible()) {
+ return false;
+ }
+
+ return VideoProfile.isTransmissionEnabled(call.getVideoState());
+ }
+
+ public static boolean isIncomingVideoCall(DialerCall call) {
+ if (!VideoUtils.isVideoCall(call)) {
+ return false;
+ }
+ final int state = call.getState();
+ return (state == DialerCall.State.INCOMING) || (state == DialerCall.State.CALL_WAITING);
+ }
+
+ public static boolean isActiveVideoCall(DialerCall call) {
+ return VideoUtils.isVideoCall(call) && call.getState() == DialerCall.State.ACTIVE;
+ }
+
+ public static boolean isOutgoingVideoCall(DialerCall call) {
+ if (!VideoUtils.isVideoCall(call)) {
+ return false;
+ }
+ final int state = call.getState();
+ return DialerCall.State.isDialing(state)
+ || state == DialerCall.State.CONNECTING
+ || state == DialerCall.State.SELECT_PHONE_ACCOUNT;
+ }
+
+ public static boolean isAudioCall(DialerCall call) {
+ if (!CompatUtils.isVideoCompatible()) {
+ return true;
+ }
+
+ return call != null && VideoProfile.isAudioOnly(call.getVideoState());
+ }
+
+ // TODO (ims-vt) Check if special handling is needed for CONF calls.
+ public static boolean canVideoPause(DialerCall call) {
+ return isVideoCall(call) && call.getState() == DialerCall.State.ACTIVE;
+ }
+
+ public static VideoProfile makeVideoPauseProfile(@NonNull DialerCall call) {
+ Objects.requireNonNull(call);
+ if (VideoProfile.isAudioOnly(call.getVideoState())) {
+ throw new IllegalStateException();
+ }
+ return new VideoProfile(getPausedVideoState(call.getVideoState()));
+ }
+
+ public static VideoProfile makeVideoUnPauseProfile(@NonNull DialerCall call) {
+ Objects.requireNonNull(call);
+ return new VideoProfile(getUnPausedVideoState(call.getVideoState()));
+ }
+
+ public static int getUnPausedVideoState(int videoState) {
+ return videoState & (~VideoProfile.STATE_PAUSED);
+ }
+
+ public static int getPausedVideoState(int videoState) {
+ return videoState | VideoProfile.STATE_PAUSED;
+ }
+
+ public static boolean hasCameraPermissionAndAllowedByUser(@NonNull Context context) {
+ return isCameraAllowedByUser(context) && hasCameraPermission(context);
+ }
+
+ public static boolean hasCameraPermission(@NonNull Context context) {
+ return ContextCompat.checkSelfPermission(context, android.Manifest.permission.CAMERA)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ public static boolean isCameraAllowedByUser(@NonNull Context context) {
+ return DialerUtils.getDefaultSharedPreferenceForDeviceProtectedStorageContext(context)
+ .getBoolean(PREFERENCE_CAMERA_ALLOWED_BY_USER, false);
+ }
+
+ public static void setCameraAllowedByUser(@NonNull Context context) {
+ DialerUtils.getDefaultSharedPreferenceForDeviceProtectedStorageContext(context)
+ .edit()
+ .putBoolean(PREFERENCE_CAMERA_ALLOWED_BY_USER, true)
+ .apply();
+ }
+}
diff --git a/java/com/android/incallui/commontheme/AndroidManifest.xml b/java/com/android/incallui/commontheme/AndroidManifest.xml
new file mode 100644
index 000000000..1d5914f07
--- /dev/null
+++ b/java/com/android/incallui/commontheme/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.incallui.commontheme">
+</manifest>
diff --git a/java/com/android/incallui/commontheme/res/animator/button_state.xml b/java/com/android/incallui/commontheme/res/animator/button_state.xml
new file mode 100644
index 000000000..70958d610
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/animator/button_state.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true" android:state_enabled="true">
+ <set>
+ <objectAnimator android:propertyName="translationZ"
+ android:duration="100"
+ android:valueTo="4dp"
+ android:valueType="floatType"/>
+ <objectAnimator android:propertyName="elevation"
+ android:duration="0"
+ android:valueTo="@dimen/incall_call_button_elevation"
+ android:valueType="floatType"/>
+ </set>
+ </item>
+ <!-- base state -->
+ <item android:state_enabled="true">
+ <set>
+ <objectAnimator android:propertyName="translationZ"
+ android:duration="100"
+ android:valueTo="0"
+ android:startDelay="100"
+ android:valueType="floatType"/>
+ <objectAnimator android:propertyName="elevation"
+ android:duration="0"
+ android:valueTo="@dimen/incall_call_button_elevation"
+ android:valueType="floatType" />
+ </set>
+ </item>
+ ...
+</selector> \ No newline at end of file
diff --git a/java/com/android/incallui/commontheme/res/animator/disabled_alpha.xml b/java/com/android/incallui/commontheme/res/animator/disabled_alpha.xml
new file mode 100644
index 000000000..8d78f0017
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/animator/disabled_alpha.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_enabled="false">
+ <set>
+ <objectAnimator
+ android:propertyName="alpha"
+ android:duration="@android:integer/config_shortAnimTime"
+ android:valueTo=".3f"
+ android:valueType="floatType"/>
+ </set>
+ </item>
+ <item>
+ <set>
+ <objectAnimator
+ android:propertyName="alpha"
+ android:duration="@android:integer/config_shortAnimTime"
+ android:valueTo="1f"
+ android:valueType="floatType"/>
+ </set>
+ </item>
+</selector>
diff --git a/java/com/android/incallui/commontheme/res/color/incall_button_ripple.xml b/java/com/android/incallui/commontheme/res/color/incall_button_ripple.xml
new file mode 100644
index 000000000..cd474c5e5
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/color/incall_button_ripple.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="#80888888" android:state_checked="true"/>
+ <item android:color="#80ffffff"/>
+</selector>
diff --git a/java/com/android/incallui/commontheme/res/color/incall_button_white.xml b/java/com/android/incallui/commontheme/res/color/incall_button_white.xml
new file mode 100644
index 000000000..5df441ff0
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/color/incall_button_white.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@android:color/white" android:state_enabled="true"/>
+ <item android:color="#99ffffff" android:state_enabled="false"/>
+</selector>
diff --git a/java/com/android/incallui/commontheme/res/drawable-hdpi/ic_phone_audio_white_36dp.png b/java/com/android/incallui/commontheme/res/drawable-hdpi/ic_phone_audio_white_36dp.png
new file mode 100644
index 000000000..26f3fe001
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/drawable-hdpi/ic_phone_audio_white_36dp.png
Binary files differ
diff --git a/java/com/android/incallui/commontheme/res/drawable-mdpi/ic_phone_audio_white_36dp.png b/java/com/android/incallui/commontheme/res/drawable-mdpi/ic_phone_audio_white_36dp.png
new file mode 100644
index 000000000..5b0a9d663
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/drawable-mdpi/ic_phone_audio_white_36dp.png
Binary files differ
diff --git a/java/com/android/incallui/commontheme/res/drawable-xhdpi/ic_phone_audio_white_36dp.png b/java/com/android/incallui/commontheme/res/drawable-xhdpi/ic_phone_audio_white_36dp.png
new file mode 100644
index 000000000..d595b190d
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/drawable-xhdpi/ic_phone_audio_white_36dp.png
Binary files differ
diff --git a/java/com/android/incallui/commontheme/res/drawable-xxhdpi/ic_phone_audio_white_36dp.png b/java/com/android/incallui/commontheme/res/drawable-xxhdpi/ic_phone_audio_white_36dp.png
new file mode 100644
index 000000000..fb7cf161b
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/drawable-xxhdpi/ic_phone_audio_white_36dp.png
Binary files differ
diff --git a/java/com/android/incallui/commontheme/res/drawable-xxxhdpi/ic_phone_audio_white_36dp.png b/java/com/android/incallui/commontheme/res/drawable-xxxhdpi/ic_phone_audio_white_36dp.png
new file mode 100644
index 000000000..4bb58d9f5
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/drawable-xxxhdpi/ic_phone_audio_white_36dp.png
Binary files differ
diff --git a/java/com/android/incallui/commontheme/res/drawable/answer_answer_background.xml b/java/com/android/incallui/commontheme/res/drawable/answer_answer_background.xml
new file mode 100644
index 000000000..090506aa6
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/drawable/answer_answer_background.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="#80FFFFFF">
+ <item>
+ <shape
+ android:shape="oval">
+ <solid android:color="#09ad00"/>
+ </shape>
+ </item>
+</ripple>
diff --git a/java/com/android/incallui/commontheme/res/drawable/answer_decline_background.xml b/java/com/android/incallui/commontheme/res/drawable/answer_decline_background.xml
new file mode 100644
index 000000000..abfd56ecf
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/drawable/answer_decline_background.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="#80FFFFFF">
+ <item>
+ <shape
+ android:shape="oval">
+ <solid android:color="#DF0000"/>
+ </shape>
+ </item>
+</ripple>
diff --git a/java/com/android/incallui/commontheme/res/drawable/incall_end_call_background.xml b/java/com/android/incallui/commontheme/res/drawable/incall_end_call_background.xml
new file mode 100644
index 000000000..3c9f4bc0b
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/drawable/incall_end_call_background.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="#80FFFFFF">
+ <item>
+ <shape
+ android:shape="oval">
+ <solid android:color="#FFDF0000"/>
+ </shape>
+ </item>
+</ripple>
diff --git a/java/com/android/incallui/commontheme/res/values-w260dp-h520dp/dimens.xml b/java/com/android/incallui/commontheme/res/values-w260dp-h520dp/dimens.xml
new file mode 100644
index 000000000..e1390597a
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/values-w260dp-h520dp/dimens.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <dimen name="incall_end_call_button_size">64dp</dimen>
+ <drawable name="incall_end_call_icon">@drawable/quantum_ic_call_end_white_36</drawable>
+</resources>
diff --git a/java/com/android/incallui/commontheme/res/values-w520dp-h260dp-land/dimens.xml b/java/com/android/incallui/commontheme/res/values-w520dp-h260dp-land/dimens.xml
new file mode 100644
index 000000000..e1390597a
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/values-w520dp-h260dp-land/dimens.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <dimen name="incall_end_call_button_size">64dp</dimen>
+ <drawable name="incall_end_call_icon">@drawable/quantum_ic_call_end_white_36</drawable>
+</resources>
diff --git a/java/com/android/incallui/commontheme/res/values/colors.xml b/java/com/android/incallui/commontheme/res/values/colors.xml
new file mode 100644
index 000000000..d38e34716
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/values/colors.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- 50% black background drawn over the video to make it easier to see text and buttons. -->
+ <color name="videocall_overlay_background_color">#7E000000</color>
+</resources> \ No newline at end of file
diff --git a/java/com/android/incallui/commontheme/res/values/dimens.xml b/java/com/android/incallui/commontheme/res/values/dimens.xml
new file mode 100644
index 000000000..649ba2cde
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/values/dimens.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <dimen name="incall_end_call_button_size">48dp</dimen>
+ <dimen name="incall_call_button_elevation">8dp</dimen>
+ <drawable name="incall_end_call_icon">@drawable/quantum_ic_call_end_white_24</drawable>
+</resources>
diff --git a/java/com/android/incallui/commontheme/res/values/strings.xml b/java/com/android/incallui/commontheme/res/values/strings.xml
new file mode 100644
index 000000000..6f346a34d
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/values/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="incall_content_description_end_call">End call</string>
+
+ <string name="incall_content_description_muted">Muted</string>
+
+ <string name="incall_content_description_unmuted">Unmuted</string>
+
+ <string name="incall_content_description_swap_calls">Swap calls</string>
+
+ <string name="incall_content_description_merge_calls">Merge calls</string>
+
+ <string name="incall_content_description_earpiece">Handset earpiece</string>
+
+ <string name="incall_content_description_speaker">Speaker</string>
+
+ <string name="incall_content_description_bluetooth">Bluetooth</string>
+
+ <string name="incall_content_description_headset">Wired headset</string>
+
+ <!-- Text for the onscreen "Hold" button when it is not selected. Pressing it will put
+ the call on hold. -->
+ <string name="incall_content_description_hold">Hold call</string>
+ <!-- Text for the onscreen "Hold" button when it is selected. Pressing it will resume
+ the call from a previously held state. -->
+ <string name="incall_content_description_unhold">Resume call</string>
+
+ <string name="incall_content_description_video_on">Video on</string>
+
+ <string name="incall_content_description_video_off">Video off</string>
+
+ <string name="incall_content_description_swap_video">Swap video</string>
+
+</resources>
diff --git a/java/com/android/incallui/commontheme/res/values/styles.xml b/java/com/android/incallui/commontheme/res/values/styles.xml
new file mode 100644
index 000000000..311f9cf4b
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/values/styles.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+
+ <style name="Dialer.Incall.TextAppearance.Large">
+ <item name="android:textColor">?android:textColorPrimary</item>
+ <item name="android:textSize">36sp</item>
+ <item name="android:fontFamily">sans-serif-light</item>
+ </style>
+
+ <style name="Dialer.Incall.TextAppearance.Label">
+ <item name="android:textColor">?android:textColorPrimary</item>
+ <item name="android:textSize">12sp</item>
+ </style>
+
+ <style name="Dialer.Incall.TextAppearance" parent="android:TextAppearance.Material">
+ <item name="android:textColor">?android:textColorSecondary</item>
+ <item name="android:textSize">18sp</item>
+ </style>
+
+ <style name="Incall.Button.End" parent="android:Widget.Material.Button">
+ <item name="android:background">@drawable/incall_end_call_background</item>
+ <item name="android:elevation">8dp</item>
+ <item name="android:layout_height">@dimen/incall_end_call_button_size</item>
+ <item name="android:layout_width">@dimen/incall_end_call_button_size</item>
+ <item name="android:padding">8dp</item>
+ <item name="android:src">@drawable/incall_end_call_icon</item>
+ <item name="android:stateListAnimator">@animator/disabled_alpha</item>
+ </style>
+
+ <style name="Answer.Button" parent="android:Widget.Material.Button">
+ <item name="android:stateListAnimator">@animator/button_state</item>
+ </style>
+
+ <style name="Answer.Button.Answer">
+ <item name="android:background">@drawable/answer_answer_background</item>
+ </style>
+
+ <style name="Answer.Button.Decline">
+ <item name="android:background">@drawable/answer_decline_background</item>
+ </style>
+
+</resources>
diff --git a/java/com/android/incallui/contactgrid/AndroidManifest.xml b/java/com/android/incallui/contactgrid/AndroidManifest.xml
new file mode 100644
index 000000000..520010548
--- /dev/null
+++ b/java/com/android/incallui/contactgrid/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.incallui.contactgrid">
+</manifest>
diff --git a/java/com/android/incallui/contactgrid/BottomRow.java b/java/com/android/incallui/contactgrid/BottomRow.java
new file mode 100644
index 000000000..aaf7e8214
--- /dev/null
+++ b/java/com/android/incallui/contactgrid/BottomRow.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.contactgrid;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.telephony.PhoneNumberUtils;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.incallui.call.DialerCall.State;
+import com.android.incallui.incall.protocol.PrimaryCallState;
+import com.android.incallui.incall.protocol.PrimaryInfo;
+
+/**
+ * Gets the content of the bottom row. For example:
+ *
+ * <ul>
+ * <li>Mobile +1 (650) 253-0000
+ * <li>[HD icon] 00:15
+ * <li>Call ended
+ * <li>Hanging up
+ * </ul>
+ */
+public class BottomRow {
+
+ /** Content of the bottom row. */
+ public static class Info {
+
+ @Nullable public final CharSequence label;
+ public final boolean isTimerVisible;
+ public final boolean isWorkIconVisible;
+ public final boolean isHdIconVisible;
+ public final boolean isForwardIconVisible;
+ public final boolean isSpamIconVisible;
+ public final boolean shouldPopulateAccessibilityEvent;
+
+ public Info(
+ @Nullable CharSequence label,
+ boolean isTimerVisible,
+ boolean isWorkIconVisible,
+ boolean isHdIconVisible,
+ boolean isForwardIconVisible,
+ boolean isSpamIconVisible,
+ boolean shouldPopulateAccessibilityEvent) {
+ this.label = label;
+ this.isTimerVisible = isTimerVisible;
+ this.isWorkIconVisible = isWorkIconVisible;
+ this.isHdIconVisible = isHdIconVisible;
+ this.isForwardIconVisible = isForwardIconVisible;
+ this.isSpamIconVisible = isSpamIconVisible;
+ this.shouldPopulateAccessibilityEvent = shouldPopulateAccessibilityEvent;
+ }
+ }
+
+ private BottomRow() {}
+
+ public static Info getInfo(Context context, PrimaryCallState state, PrimaryInfo primaryInfo) {
+ CharSequence label;
+ boolean isTimerVisible = state.state == State.ACTIVE;
+ boolean isForwardIconVisible = state.isForwardedNumber;
+ boolean isWorkIconVisible = state.isWorkCall;
+ boolean isHdIconVisible = state.isHdAudioCall && !isForwardIconVisible;
+ boolean isSpamIconVisible = false;
+ boolean shouldPopulateAccessibilityEvent = true;
+
+ if (isIncoming(state) && primaryInfo.isSpam) {
+ label = context.getString(R.string.contact_grid_incoming_suspected_spam);
+ isSpamIconVisible = true;
+ isHdIconVisible = false;
+ } else if (state.state == State.DISCONNECTING) {
+ // While in the DISCONNECTING state we display a "Hanging up" message in order to make the UI
+ // feel more responsive. (In GSM it's normal to see a delay of a couple of seconds while
+ // negotiating the disconnect with the network, so the "Hanging up" state at least lets the
+ // user know that we're doing something. This state is currently not used with CDMA.)
+ label = context.getString(R.string.incall_hanging_up);
+ } else if (state.state == State.DISCONNECTED) {
+ label = state.disconnectCause.getLabel();
+ if (TextUtils.isEmpty(label)) {
+ label = context.getString(R.string.incall_call_ended);
+ }
+ } else if (!TextUtils.isEmpty(state.callbackNumber)) {
+ // This is used for carriers like Project Fi to show the callback number for emergency calls.
+ label =
+ context.getString(
+ R.string.contact_grid_callback_number,
+ PhoneNumberUtils.formatNumber(state.callbackNumber));
+ isTimerVisible = false;
+ } else {
+ label = getLabelForPhoneNumber(primaryInfo);
+ shouldPopulateAccessibilityEvent = primaryInfo.nameIsNumber;
+ }
+
+ return new Info(
+ label,
+ isTimerVisible,
+ isWorkIconVisible,
+ isHdIconVisible,
+ isForwardIconVisible,
+ isSpamIconVisible,
+ shouldPopulateAccessibilityEvent);
+ }
+
+ private static CharSequence getLabelForPhoneNumber(PrimaryInfo primaryInfo) {
+ if (primaryInfo.nameIsNumber) {
+ return primaryInfo.location;
+ }
+ if (!TextUtils.isEmpty(primaryInfo.number)) {
+ CharSequence spannedNumber = spanDisplayNumber(primaryInfo.number);
+ if (primaryInfo.label == null) {
+ return spannedNumber;
+ } else {
+ return TextUtils.concat(primaryInfo.label, " ", spannedNumber);
+ }
+ }
+ return null;
+ }
+
+ private static CharSequence spanDisplayNumber(String displayNumber) {
+ return PhoneNumberUtilsCompat.createTtsSpannable(
+ BidiFormatter.getInstance().unicodeWrap(displayNumber, TextDirectionHeuristics.LTR));
+ }
+
+ private static boolean isIncoming(PrimaryCallState state) {
+ return state.state == State.INCOMING || state.state == State.CALL_WAITING;
+ }
+}
diff --git a/java/com/android/incallui/contactgrid/ContactGridManager.java b/java/com/android/incallui/contactgrid/ContactGridManager.java
new file mode 100644
index 000000000..81c225163
--- /dev/null
+++ b/java/com/android/incallui/contactgrid/ContactGridManager.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.contactgrid;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.Chronometer;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.ViewAnimator;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.contacts.common.lettertiles.LetterTileDrawable;
+import com.android.dialer.common.Assert;
+import com.android.dialer.util.DrawableConverter;
+import com.android.incallui.incall.protocol.ContactPhotoType;
+import com.android.incallui.incall.protocol.PrimaryCallState;
+import com.android.incallui.incall.protocol.PrimaryInfo;
+import java.util.List;
+
+/** Utility to manage the Contact grid */
+public class ContactGridManager {
+
+ private final Context context;
+ private final View contactGridLayout;
+
+ // Row 0: Captain Holt ON HOLD
+ // Row 0: Calling...
+ // Row 0: [Wi-Fi icon] Calling via Starbucks Wi-Fi
+ // Row 0: [Wi-Fi icon] Starbucks Wi-Fi
+ // Row 0: Hey Jake, pick up!
+ private ImageView connectionIconImageView;
+ private TextView statusTextView;
+
+ // Row 1: Jake Peralta [Contact photo]
+ // Row 1: Walgreens
+ // Row 1: +1 (650) 253-0000
+ private TextView contactNameTextView;
+ @Nullable private ImageView avatarImageView;
+
+ // Row 2: Mobile +1 (650) 253-0000
+ // Row 2: [HD icon] 00:15
+ // Row 2: Call ended
+ // Row 2: Hanging up
+ // Row 2: [Alert sign] Suspected spam caller
+ // Row 2: Your emergency callback number: +1 (650) 253-0000
+ private ImageView workIconImageView;
+ private ImageView hdIconImageView;
+ private ImageView forwardIconImageView;
+ private ImageView spamIconImageView;
+ private ViewAnimator bottomTextSwitcher;
+ private TextView bottomTextView;
+ private Chronometer bottomTimerView;
+ private int avatarSize;
+ private boolean hideAvatar;
+ private boolean showAnonymousAvatar;
+ private boolean middleRowVisible = true;
+
+ private PrimaryInfo primaryInfo = PrimaryInfo.createEmptyPrimaryInfo();
+ private PrimaryCallState primaryCallState = PrimaryCallState.createEmptyPrimaryCallState();
+ private final LetterTileDrawable letterTile;
+
+
+ public ContactGridManager(
+ View view, @Nullable ImageView avatarImageView, int avatarSize, boolean showAnonymousAvatar) {
+ context = view.getContext();
+ Assert.isNotNull(context);
+
+ this.avatarImageView = avatarImageView;
+ this.avatarSize = avatarSize;
+ this.showAnonymousAvatar = showAnonymousAvatar;
+ connectionIconImageView = (ImageView) view.findViewById(R.id.contactgrid_connection_icon);
+ statusTextView = (TextView) view.findViewById(R.id.contactgrid_status_text);
+ contactNameTextView = (TextView) view.findViewById(R.id.contactgrid_contact_name);
+ workIconImageView = (ImageView) view.findViewById(R.id.contactgrid_workIcon);
+ hdIconImageView = (ImageView) view.findViewById(R.id.contactgrid_hdIcon);
+ forwardIconImageView = (ImageView) view.findViewById(R.id.contactgrid_forwardIcon);
+ spamIconImageView = (ImageView) view.findViewById(R.id.contactgrid_spamIcon);
+ bottomTextSwitcher = (ViewAnimator) view.findViewById(R.id.contactgrid_bottom_text_switcher);
+ bottomTextView = (TextView) view.findViewById(R.id.contactgrid_bottom_text);
+ bottomTimerView = (Chronometer) view.findViewById(R.id.contactgrid_bottom_timer);
+
+ contactGridLayout = (View) contactNameTextView.getParent();
+ letterTile = new LetterTileDrawable(context.getResources());
+ }
+
+ public void show() {
+ contactGridLayout.setVisibility(View.VISIBLE);
+ }
+
+ public void hide() {
+ contactGridLayout.setVisibility(View.GONE);
+ }
+
+ public void setAvatarHidden(boolean hide) {
+ if (hide != hideAvatar) {
+ hideAvatar = hide;
+ updatePrimaryNameAndPhoto();
+ }
+ }
+
+ public boolean isAvatarHidden() {
+ return hideAvatar;
+ }
+
+ public View getContainerView() {
+ return contactGridLayout;
+ }
+
+ public void setIsMiddleRowVisible(boolean isMiddleRowVisible) {
+ if (middleRowVisible == isMiddleRowVisible) {
+ return;
+ }
+ middleRowVisible = isMiddleRowVisible;
+
+ contactNameTextView.setVisibility(isMiddleRowVisible ? View.VISIBLE : View.GONE);
+ updateAvatarVisibility();
+ }
+
+ public void setPrimary(PrimaryInfo primaryInfo) {
+ this.primaryInfo = primaryInfo;
+ updatePrimaryNameAndPhoto();
+ updateBottomRow();
+ }
+
+ public void setCallState(PrimaryCallState primaryCallState) {
+ this.primaryCallState = primaryCallState;
+ updatePrimaryNameAndPhoto();
+ updateBottomRow();
+ updateTopRow();
+ }
+
+ public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ dispatchPopulateAccessibilityEvent(event, statusTextView);
+ dispatchPopulateAccessibilityEvent(event, contactNameTextView);
+ BottomRow.Info info = BottomRow.getInfo(context, primaryCallState, primaryInfo);
+ if (info.shouldPopulateAccessibilityEvent) {
+ dispatchPopulateAccessibilityEvent(event, bottomTextView);
+ }
+ }
+
+ public void setAvatarImageView(
+ @Nullable ImageView avatarImageView, int avatarSize, boolean showAnonymousAvatar) {
+ this.avatarImageView = avatarImageView;
+ this.avatarSize = avatarSize;
+ this.showAnonymousAvatar = showAnonymousAvatar;
+ updatePrimaryNameAndPhoto();
+ }
+
+ private void dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view) {
+ final List<CharSequence> eventText = event.getText();
+ int size = eventText.size();
+ view.dispatchPopulateAccessibilityEvent(event);
+ // If no text added write null to keep relative position.
+ if (size == eventText.size()) {
+ eventText.add(null);
+ }
+ }
+
+ private boolean updateAvatarVisibility() {
+ if (avatarImageView == null) {
+ return false;
+ }
+
+ if (!middleRowVisible) {
+ avatarImageView.setVisibility(View.GONE);
+ return false;
+ }
+
+ boolean hasPhoto =
+ primaryInfo.photo != null && primaryInfo.photoType == ContactPhotoType.CONTACT;
+ if (!hasPhoto && !showAnonymousAvatar) {
+ avatarImageView.setVisibility(View.GONE);
+ return false;
+ }
+
+ avatarImageView.setVisibility(View.VISIBLE);
+ return true;
+ }
+
+ /**
+ * Updates row 0. For example:
+ *
+ * <ul>
+ * <li>Captain Holt ON HOLD
+ * <li>Calling...
+ * <li>[Wi-Fi icon] Calling via Starbucks Wi-Fi
+ * <li>[Wi-Fi icon] Starbucks Wi-Fi
+ * <li>Call from
+ * </ul>
+ */
+ private void updateTopRow() {
+ TopRow.Info info = TopRow.getInfo(context, primaryCallState);
+ if (TextUtils.isEmpty(info.label)) {
+ // Use INVISIBLE here to prevent the rows below this one from moving up and down.
+ statusTextView.setVisibility(View.INVISIBLE);
+ statusTextView.setText(null);
+ } else {
+ statusTextView.setText(info.label);
+ statusTextView.setVisibility(View.VISIBLE);
+ statusTextView.setSingleLine(info.labelIsSingleLine);
+ }
+
+ if (info.icon == null) {
+ connectionIconImageView.setVisibility(View.GONE);
+ } else {
+ connectionIconImageView.setVisibility(View.VISIBLE);
+ connectionIconImageView.setImageDrawable(info.icon);
+ }
+ }
+
+ /**
+ * Updates row 1. For example:
+ *
+ * <ul>
+ * <li>Jake Peralta [Contact photo]
+ * <li>Walgreens
+ * <li>+1 (650) 253-0000
+ * </ul>
+ */
+ private void updatePrimaryNameAndPhoto() {
+ if (TextUtils.isEmpty(primaryInfo.name)) {
+ contactNameTextView.setText(null);
+ } else {
+ contactNameTextView.setText(
+ primaryInfo.nameIsNumber
+ ? PhoneNumberUtilsCompat.createTtsSpannable(primaryInfo.name)
+ : primaryInfo.name);
+
+ // Set direction of the name field
+ int nameDirection = View.TEXT_DIRECTION_INHERIT;
+ if (primaryInfo.nameIsNumber) {
+ nameDirection = View.TEXT_DIRECTION_LTR;
+ }
+ contactNameTextView.setTextDirection(nameDirection);
+ }
+
+ if (avatarImageView != null) {
+ if (hideAvatar) {
+ avatarImageView.setVisibility(View.GONE);
+ } else if (avatarImageView != null && avatarSize > 0 && updateAvatarVisibility()) {
+ boolean hasPhoto =
+ primaryInfo.photo != null && primaryInfo.photoType == ContactPhotoType.CONTACT;
+ // Contact has a photo, don't render a letter tile.
+ if (hasPhoto) {
+ avatarImageView.setBackground(
+ DrawableConverter.getRoundedDrawable(
+ context, primaryInfo.photo, avatarSize, avatarSize));
+ // Contact has a name, that isn't a number.
+ } else {
+ int contactType =
+ primaryCallState.isVoiceMailNumber
+ ? LetterTileDrawable.TYPE_VOICEMAIL
+ : LetterTileDrawable.TYPE_DEFAULT;
+ letterTile.setCanonicalDialerLetterTileDetails(
+ primaryInfo.name,
+ primaryInfo.contactInfoLookupKey,
+ LetterTileDrawable.SHAPE_CIRCLE,
+ contactType);
+ avatarImageView.setBackground(letterTile);
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates row 2. For example:
+ *
+ * <ul>
+ * <li>Mobile +1 (650) 253-0000
+ * <li>[HD icon] 00:15
+ * <li>Call ended
+ * <li>Hanging up
+ * </ul>
+ */
+ private void updateBottomRow() {
+ BottomRow.Info info = BottomRow.getInfo(context, primaryCallState, primaryInfo);
+
+ bottomTextView.setText(info.label);
+ bottomTextView.setAllCaps(info.isSpamIconVisible);
+ workIconImageView.setVisibility(info.isWorkIconVisible ? View.VISIBLE : View.GONE);
+ hdIconImageView.setVisibility(info.isHdIconVisible ? View.VISIBLE : View.GONE);
+ forwardIconImageView.setVisibility(info.isForwardIconVisible ? View.VISIBLE : View.GONE);
+ spamIconImageView.setVisibility(info.isSpamIconVisible ? View.VISIBLE : View.GONE);
+
+ if (info.isTimerVisible) {
+ bottomTextSwitcher.setDisplayedChild(1);
+ bottomTimerView.setBase(
+ primaryCallState.connectTimeMillis
+ - System.currentTimeMillis()
+ + SystemClock.elapsedRealtime());
+ bottomTimerView.start();
+ } else {
+ bottomTextSwitcher.setDisplayedChild(0);
+ bottomTimerView.stop();
+ }
+ }
+}
diff --git a/java/com/android/incallui/contactgrid/TopRow.java b/java/com/android/incallui/contactgrid/TopRow.java
new file mode 100644
index 000000000..a340fd0a0
--- /dev/null
+++ b/java/com/android/incallui/contactgrid/TopRow.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.contactgrid;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import com.android.dialer.common.Assert;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.State;
+import com.android.incallui.call.VideoUtils;
+import com.android.incallui.incall.protocol.PrimaryCallState;
+
+/**
+ * Gets the content of the top row. For example:
+ *
+ * <ul>
+ * <li>Captain Holt ON HOLD
+ * <li>Calling...
+ * <li>[Wi-Fi icon] Calling via Starbucks Wi-Fi
+ * <li>[Wi-Fi icon] Starbucks Wi-Fi
+ * <li>Call from
+ * </ul>
+ */
+public class TopRow {
+
+ /** Content of the top row. */
+ public static class Info {
+
+ @Nullable public final CharSequence label;
+ @Nullable public final Drawable icon;
+ public final boolean labelIsSingleLine;
+
+ public Info(@Nullable CharSequence label, @Nullable Drawable icon, boolean labelIsSingleLine) {
+ this.label = label;
+ this.icon = icon;
+ this.labelIsSingleLine = labelIsSingleLine;
+ }
+ }
+
+ private TopRow() {}
+
+ public static Info getInfo(Context context, PrimaryCallState state) {
+ CharSequence label = null;
+ Drawable icon = state.connectionIcon;
+ boolean labelIsSingleLine = true;
+
+ if (state.isWifi && icon == null) {
+ icon = context.getDrawable(R.drawable.quantum_ic_network_wifi_white_24);
+ }
+
+ if (state.state == State.INCOMING || state.state == State.CALL_WAITING) {
+ // Call from
+ // [Wi-Fi icon] Video call from
+ // Hey Jake, pick up!
+ if (!TextUtils.isEmpty(state.callSubject)) {
+ label = state.callSubject;
+ labelIsSingleLine = false;
+ } else {
+ label = getLabelForIncoming(context, state);
+ }
+ } else if (VideoUtils.hasSentVideoUpgradeRequest(state.sessionModificationState)
+ || VideoUtils.hasReceivedVideoUpgradeRequest(state.sessionModificationState)) {
+ label = getLabelForVideoRequest(context, state);
+ } else if (state.state == State.PULLING) {
+ label = context.getString(R.string.incall_transferring);
+ } else if (state.state == State.DIALING || state.state == State.CONNECTING) {
+ // [Wi-Fi icon] Calling via Google Guest
+ // Calling...
+ label = getLabelForDialing(context, state);
+ } else if (state.state == State.ACTIVE && state.isRemotelyHeld) {
+ label = context.getString(R.string.incall_remotely_held);
+ } else {
+ // Video calling...
+ // [Wi-Fi icon] Starbucks Wi-Fi
+ label = getConnectionLabel(state);
+ }
+
+ return new Info(label, icon, labelIsSingleLine);
+ }
+
+ private static CharSequence getLabelForIncoming(Context context, PrimaryCallState state) {
+ if (VideoUtils.isVideoCall(state.videoState)) {
+ return getLabelForIncomingVideo(context, state.isWifi);
+ } else if (state.isWifi && !TextUtils.isEmpty(state.connectionLabel)) {
+ return state.connectionLabel;
+ } else if (isAccount(state)) {
+ return context.getString(R.string.contact_grid_incoming_via_template, state.connectionLabel);
+ } else if (state.isWorkCall) {
+ return context.getString(R.string.contact_grid_incoming_work_call);
+ } else {
+ return context.getString(R.string.contact_grid_incoming_voice_call);
+ }
+ }
+
+ private static CharSequence getLabelForIncomingVideo(Context context, boolean isWifi) {
+ if (isWifi) {
+ return context.getString(R.string.contact_grid_incoming_wifi_video_call);
+ } else {
+ return context.getString(R.string.contact_grid_incoming_video_call);
+ }
+ }
+
+ private static CharSequence getLabelForDialing(Context context, PrimaryCallState state) {
+ if (!TextUtils.isEmpty(state.connectionLabel) && !state.isWifi) {
+ return context.getString(R.string.incall_calling_via_template, state.connectionLabel);
+ } else {
+ if (VideoUtils.isVideoCall(state.videoState)) {
+ if (state.isWifi) {
+ return context.getString(R.string.incall_wifi_video_call_requesting);
+ } else {
+ return context.getString(R.string.incall_video_call_requesting);
+ }
+ }
+ return context.getString(R.string.incall_connecting);
+ }
+ }
+
+ private static CharSequence getConnectionLabel(PrimaryCallState state) {
+ if (!TextUtils.isEmpty(state.connectionLabel)
+ && (isAccount(state) || state.isWifi || state.isConference)) {
+ // We normally don't show a "call state label" at all when active
+ // (but we can use the call state label to display the provider name).
+ return state.connectionLabel;
+ } else {
+ return null;
+ }
+ }
+
+ private static CharSequence getLabelForVideoRequest(Context context, PrimaryCallState state) {
+ switch (state.sessionModificationState) {
+ case DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE:
+ return context.getString(R.string.incall_video_call_requesting);
+ case DialerCall.SESSION_MODIFICATION_STATE_REQUEST_FAILED:
+ case DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED:
+ return context.getString(R.string.incall_video_call_request_failed);
+ case DialerCall.SESSION_MODIFICATION_STATE_REQUEST_REJECTED:
+ return context.getString(R.string.incall_video_call_request_rejected);
+ case DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT:
+ return context.getString(R.string.incall_video_call_request_timed_out);
+ case DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST:
+ return getLabelForIncomingVideo(context, state.isWifi);
+ case DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST:
+ default:
+ Assert.fail();
+ return null;
+ }
+ }
+
+ private static boolean isAccount(PrimaryCallState state) {
+ return !TextUtils.isEmpty(state.connectionLabel) && TextUtils.isEmpty(state.gatewayNumber);
+ }
+}
diff --git a/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml b/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml
new file mode 100644
index 000000000..3900be556
--- /dev/null
+++ b/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_horizontal"
+ tools:showIn="@layout/incall_contact_grid">
+ <ImageView
+ android:id="@id/contactgrid_workIcon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_marginEnd="8dp"
+ android:scaleType="fitCenter"
+ android:src="@drawable/ic_work_profile"
+ android:tint="#ffffff"
+ tools:visibility="gone"
+ />
+ <ImageView
+ android:id="@id/contactgrid_hdIcon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_marginEnd="8dp"
+ android:scaleType="fitCenter"
+ android:src="@drawable/quantum_ic_hd_white_24"
+ tools:visibility="gone"
+ />
+ <ImageView
+ android:id="@id/contactgrid_forwardIcon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_marginEnd="8dp"
+ android:scaleType="fitCenter"
+ android:src="@drawable/quantum_ic_forward_white_24"
+ tools:visibility="gone"
+ />
+ <ImageView
+ android:id="@+id/contactgrid_spamIcon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_marginEnd="8dp"
+ android:scaleType="fitCenter"
+ android:src="@drawable/quantum_ic_report_white_18"
+ tools:visibility="gone"
+ />
+ <ViewAnimator
+ android:id="@+id/contactgrid_bottom_text_switcher"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="2dp"
+ android:measureAllChildren="false">
+ <TextView
+ android:id="@+id/contactgrid_bottom_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:singleLine="true"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance"
+ tools:gravity="start"
+ tools:text="Mobile +1 (650) 253-0000"/>
+ <Chronometer
+ android:id="@+id/contactgrid_bottom_timer"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:singleLine="true"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance"
+ tools:gravity="center"/>
+ </ViewAnimator>
+</LinearLayout>
diff --git a/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_top_row.xml b/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_top_row.xml
new file mode 100644
index 000000000..59359c9c1
--- /dev/null
+++ b/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_top_row.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal"
+ tools:showIn="@layout/incall_contact_grid">
+ <ImageView
+ android:id="@id/contactgrid_connection_icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_marginEnd="10dp"
+ android:scaleType="fitCenter"
+ tools:src="@android:drawable/sym_def_app_icon"
+ tools:visibility="visible"
+ />
+ <TextView
+ android:id="@id/contactgrid_status_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance"
+ tools:text="Captain Holt"/>
+</LinearLayout>
diff --git a/java/com/android/incallui/contactgrid/res/values/ids.xml b/java/com/android/incallui/contactgrid/res/values/ids.xml
new file mode 100644
index 000000000..821dc9d98
--- /dev/null
+++ b/java/com/android/incallui/contactgrid/res/values/ids.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <item name="contactgrid_connection_icon" type="id"/>
+ <item name="contactgrid_status_text" type="id"/>
+ <item name="contactgrid_contact_name" type="id"/>
+ <item name="contactgrid_workIcon" type="id"/>
+ <item name="contactgrid_hdIcon" type="id"/>
+ <item name="contactgrid_forwardIcon" type="id"/>
+ <item name="contactgrid_spamIcon" type="id"/>
+ <item name="contactgrid_bottom_text" type="id"/>
+ <item name="contactgrid_bottom_timer" type="id"/>
+ <item name="contactgrid_avatar" type="id"/>
+ <item name="contactgrid_top_row" type="id"/>
+ <item name="contactgrid_bottom_row" type="id"/>
+</resources>
diff --git a/java/com/android/incallui/contactgrid/res/values/strings.xml b/java/com/android/incallui/contactgrid/res/values/strings.xml
new file mode 100644
index 000000000..385f843b1
--- /dev/null
+++ b/java/com/android/incallui/contactgrid/res/values/strings.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- Title displayed in the overlay for outgoing calls which include the name of the provider.
+ [CHAR LIMIT=40] -->
+ <string name="incall_calling_via_template">Calling via <xliff:g id="provider_name">%s</xliff:g></string>
+
+ <!-- Displayed above the contact name during an outgoing phone call. Indicates that the call is
+ in the connecting stage. -->
+ <string name="incall_connecting">Calling…</string>
+
+ <!-- Displayed above the contact name when an external call is being pulled to the local
+ device. -->
+ <string name="incall_transferring">Transferring…</string>
+
+ <!-- Displayed above the contact name when the user requests an upgrade from a voice call to a
+ video call. -->
+ <string name="incall_video_call_requesting">Video calling…</string>
+
+ <!-- Displayed above the contact name when the user requests an upgrade from a voice call to a
+ Wi-Fi video call. -->
+ <string name="incall_wifi_video_call_requesting">Wi-Fi video calling…</string>
+
+ <!-- Displayed above the contact name when the user's video upgrade failed due to an unknown
+ reason. -->
+ <string name="incall_video_call_request_failed">Unable to connect</string>
+
+ <!-- Displayed above the contact name when the user's video upgrade was declined by the remote
+ party. -->
+ <string name="incall_video_call_request_rejected">Call declined</string>
+
+ <!-- Displayed above the contact name when no response was received for the user's upgrade
+ requests and we timed out. -->
+ <string name="incall_video_call_request_timed_out">Call timed out</string>
+
+ <!-- In-call screen: status label for a call that's in the process of hanging up
+ [CHAR LIMIT=25] -->
+ <string name="incall_hanging_up">Hanging up</string>
+
+ <!-- In-call screen: status label displayed briefly after a call ends [CHAR LIMIT=25] -->
+ <string name="incall_call_ended">Call ended</string>
+
+ <!-- In-call screen: label shown at the top of the screen when a call is on hold by the remote
+ party [CHAR LIMIT=25] -->
+ <string name="incall_remotely_held">On hold</string>
+
+ <!-- Displayed in the answer call screen for incoming video calls. -->
+ <string name="contact_grid_incoming_video_call">Video call from</string>
+
+ <!-- Displayed in the answer call screen for incoming video calls over Wi-F. -->
+ <string name="contact_grid_incoming_wifi_video_call">Wi-Fi video call from</string>
+
+ <!-- Displayed in the answer call screen for incoming voice calls. -->
+ <string name="contact_grid_incoming_voice_call">Call from</string>
+
+ <!-- Displayed in the answer call screen for incoming voice calls. -->
+ <string name="contact_grid_incoming_work_call">Work call from</string>
+
+ <!-- Displayed in the answer call screen for incoming calls via a phone account. -->
+ <string name="contact_grid_incoming_via_template">Incoming via <xliff:g id="provider_name">%s</xliff:g></string>
+
+ <!-- Displayed in the answer call screen for incoming spam calls. -->
+ <string name="contact_grid_incoming_suspected_spam">Suspected spam caller</string>
+
+ <!-- In-call screen: string shown to the user when their outgoing number is different than the
+ number reported by TelephonyManager#getLine1Number(). This is used for carriers like
+ Project Fi so that users can give their number to emergency responders. -->
+ <string name="contact_grid_callback_number">Callback number: <xliff:g id="dark_number">%1$s</xliff:g></string>
+</resources>
diff --git a/java/com/android/incallui/hold/AndroidManifest.xml b/java/com/android/incallui/hold/AndroidManifest.xml
new file mode 100644
index 000000000..2aedce903
--- /dev/null
+++ b/java/com/android/incallui/hold/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.incallui.hold">
+</manifest>
diff --git a/java/com/android/incallui/hold/OnHoldFragment.java b/java/com/android/incallui/hold/OnHoldFragment.java
new file mode 100644
index 000000000..c6952131b
--- /dev/null
+++ b/java/com/android/incallui/hold/OnHoldFragment.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.hold;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.telephony.PhoneNumberUtils;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.transition.TransitionManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnAttachStateChangeListener;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.android.dialer.common.Assert;
+import com.android.incallui.incall.protocol.SecondaryInfo;
+
+/** Shows banner UI for background call */
+public class OnHoldFragment extends Fragment {
+
+ private static final String ARG_INFO = "info";
+ private boolean padTopInset = true;
+ private int topInset;
+
+ public static OnHoldFragment newInstance(@NonNull SecondaryInfo info) {
+ OnHoldFragment fragment = new OnHoldFragment();
+ Bundle args = new Bundle();
+ args.putParcelable(ARG_INFO, info);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+ final View view = layoutInflater.inflate(R.layout.incall_on_hold_banner, viewGroup, false);
+
+ SecondaryInfo secondaryInfo = getArguments().getParcelable(ARG_INFO);
+ secondaryInfo = Assert.isNotNull(secondaryInfo);
+
+ ((TextView) view.findViewById(R.id.hold_contact_name))
+ .setText(
+ secondaryInfo.nameIsNumber
+ ? PhoneNumberUtils.createTtsSpannable(
+ BidiFormatter.getInstance()
+ .unicodeWrap(secondaryInfo.name, TextDirectionHeuristics.LTR))
+ : secondaryInfo.name);
+ ((ImageView) view.findViewById(R.id.hold_phone_icon))
+ .setImageResource(
+ secondaryInfo.isVideoCall
+ ? R.drawable.quantum_ic_videocam_white_18
+ : R.drawable.quantum_ic_call_white_18);
+ view.addOnAttachStateChangeListener(
+ new OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ topInset = v.getRootWindowInsets().getSystemWindowInsetTop();
+ applyInset();
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {}
+ });
+ return view;
+ }
+
+ public void setPadTopInset(boolean padTopInset) {
+ this.padTopInset = padTopInset;
+ applyInset();
+ }
+
+ private void applyInset() {
+ if (getView() == null) {
+ return;
+ }
+
+ int newPadding = padTopInset ? topInset : 0;
+ if (newPadding != getView().getPaddingTop()) {
+ TransitionManager.beginDelayedTransition(((ViewGroup) getView().getParent()));
+ getView().setPadding(0, newPadding, 0, 0);
+ }
+ }
+}
diff --git a/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml b/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml
new file mode 100644
index 000000000..c213af5da
--- /dev/null
+++ b/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="#CC212121"
+ android:fitsSystemWindows="true">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="24dp"
+ android:paddingEnd="24dp"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp"
+ android:gravity="center_vertical">
+
+ <ImageView
+ android:id="@+id/hold_phone_icon"
+ android:layout_width="18dp"
+ android:layout_height="18dp"
+ android:src="@drawable/quantum_ic_call_white_18"
+ android:contentDescription="@null"/>
+
+ <TextView
+ android:id="@+id/hold_contact_name"
+ style="@style/Dialer.Incall.TextAppearance"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="24dp"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textColor="@android:color/white"
+ tools:text="Jake Peralta Really Longname"/>
+
+ <TextView
+ style="@style/Dialer.Incall.TextAppearance"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAllCaps="true"
+ android:textColor="@android:color/white"
+ android:text="@string/incall_on_hold"/>
+ </LinearLayout>
+</FrameLayout>
diff --git a/java/com/android/incallui/hold/res/values/strings.xml b/java/com/android/incallui/hold/res/values/strings.xml
new file mode 100644
index 000000000..2e66bcf6c
--- /dev/null
+++ b/java/com/android/incallui/hold/res/values/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="incall_on_hold">On hold</string>
+
+</resources>
diff --git a/java/com/android/incallui/incall/bindings/InCallBindings.java b/java/com/android/incallui/incall/bindings/InCallBindings.java
new file mode 100644
index 000000000..8bbbc68e1
--- /dev/null
+++ b/java/com/android/incallui/incall/bindings/InCallBindings.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.incall.bindings;
+
+import com.android.incallui.incall.impl.InCallFragment;
+import com.android.incallui.incall.protocol.InCallScreen;
+
+/** Bindings for the in call module. */
+public class InCallBindings {
+
+ public static InCallScreen createInCallScreen() {
+ return new InCallFragment();
+ }
+}
diff --git a/java/com/android/incallui/incall/impl/AndroidManifest.xml b/java/com/android/incallui/incall/impl/AndroidManifest.xml
new file mode 100644
index 000000000..a0e3110d8
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.incall.incall.impl">
+</manifest>
diff --git a/java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java b/java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java
new file mode 100644
index 000000000..addebc484
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.incall.impl;
+
+import javax.annotation.Generated;
+
+@Generated("com.google.auto.value.processor.AutoValueProcessor")
+ final class AutoValue_MappedButtonConfig_MappingInfo extends MappedButtonConfig.MappingInfo {
+
+ private final int slot;
+ private final int slotOrder;
+ private final int conflictOrder;
+
+ private AutoValue_MappedButtonConfig_MappingInfo(
+ int slot,
+ int slotOrder,
+ int conflictOrder) {
+ this.slot = slot;
+ this.slotOrder = slotOrder;
+ this.conflictOrder = conflictOrder;
+ }
+
+ @Override
+ public int getSlot() {
+ return slot;
+ }
+
+ @Override
+ public int getSlotOrder() {
+ return slotOrder;
+ }
+
+ @Override
+ public int getConflictOrder() {
+ return conflictOrder;
+ }
+
+ @Override
+ public String toString() {
+ return "MappingInfo{"
+ + "slot=" + slot + ", "
+ + "slotOrder=" + slotOrder + ", "
+ + "conflictOrder=" + conflictOrder
+ + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof MappedButtonConfig.MappingInfo) {
+ MappedButtonConfig.MappingInfo that = (MappedButtonConfig.MappingInfo) o;
+ return (this.slot == that.getSlot())
+ && (this.slotOrder == that.getSlotOrder())
+ && (this.conflictOrder == that.getConflictOrder());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 1;
+ h *= 1000003;
+ h ^= this.slot;
+ h *= 1000003;
+ h ^= this.slotOrder;
+ h *= 1000003;
+ h ^= this.conflictOrder;
+ return h;
+ }
+
+ static final class Builder extends MappedButtonConfig.MappingInfo.Builder {
+ private Integer slot;
+ private Integer slotOrder;
+ private Integer conflictOrder;
+ Builder() {
+ }
+ private Builder(MappedButtonConfig.MappingInfo source) {
+ this.slot = source.getSlot();
+ this.slotOrder = source.getSlotOrder();
+ this.conflictOrder = source.getConflictOrder();
+ }
+ @Override
+ public MappedButtonConfig.MappingInfo.Builder setSlot(int slot) {
+ this.slot = slot;
+ return this;
+ }
+ @Override
+ public MappedButtonConfig.MappingInfo.Builder setSlotOrder(int slotOrder) {
+ this.slotOrder = slotOrder;
+ return this;
+ }
+ @Override
+ public MappedButtonConfig.MappingInfo.Builder setConflictOrder(int conflictOrder) {
+ this.conflictOrder = conflictOrder;
+ return this;
+ }
+ @Override
+ public MappedButtonConfig.MappingInfo build() {
+ String missing = "";
+ if (this.slot == null) {
+ missing += " slot";
+ }
+ if (this.slotOrder == null) {
+ missing += " slotOrder";
+ }
+ if (this.conflictOrder == null) {
+ missing += " conflictOrder";
+ }
+ if (!missing.isEmpty()) {
+ throw new IllegalStateException("Missing required properties:" + missing);
+ }
+ return new AutoValue_MappedButtonConfig_MappingInfo(
+ this.slot,
+ this.slotOrder,
+ this.conflictOrder);
+ }
+ }
+
+}
diff --git a/java/com/android/incallui/incall/impl/ButtonChooser.java b/java/com/android/incallui/incall/impl/ButtonChooser.java
new file mode 100644
index 000000000..55b82f015
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/ButtonChooser.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.incall.impl;
+
+import android.support.annotation.NonNull;
+import com.android.dialer.common.Assert;
+import com.android.incallui.incall.protocol.InCallButtonIds;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Determines where logical buttons should be placed in the {@link InCallFragment} based on the
+ * provided mapping.
+ *
+ * <p>The button placement returned by a call to {@link #getButtonPlacement(int, Set)} is created as
+ * follows: one button is placed at each UI slot, using the provided mapping to resolve conflicts.
+ * Any allowed buttons that were not chosen for their desired slot are filled in at the end of the
+ * list until it becomes the proper size.
+ */
+@Immutable
+final class ButtonChooser {
+
+ private final MappedButtonConfig config;
+
+ public ButtonChooser(@NonNull MappedButtonConfig config) {
+ this.config = Assert.isNotNull(config);
+ }
+
+ /**
+ * Returns the buttons that should be shown in the {@link InCallFragment}, ordered appropriately.
+ *
+ * @param numUiButtons the number of ui buttons available.
+ * @param allowedButtons the {@link InCallButtonIds} that can be shown.
+ * @param disabledButtons the {@link InCallButtonIds} that can be shown but in disabled stats.
+ * @return an immutable list whose size is at most {@code numUiButtons}, containing the buttons to
+ * show.
+ */
+ @NonNull
+ public List<Integer> getButtonPlacement(
+ int numUiButtons,
+ @NonNull Set<Integer> allowedButtons,
+ @NonNull Set<Integer> disabledButtons) {
+ Assert.isNotNull(allowedButtons);
+ Assert.checkArgument(numUiButtons >= 0);
+
+ if (numUiButtons == 0 || allowedButtons.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ List<Integer> placedButtons = new ArrayList<>();
+ List<Integer> conflicts = new ArrayList<>();
+ placeButtonsInSlots(numUiButtons, allowedButtons, placedButtons, conflicts);
+ placeConflictsInOpenSlots(
+ numUiButtons, allowedButtons, disabledButtons, placedButtons, conflicts);
+ return Collections.unmodifiableList(placedButtons);
+ }
+
+ private void placeButtonsInSlots(
+ int numUiButtons,
+ @NonNull Set<Integer> allowedButtons,
+ @NonNull List<Integer> placedButtons,
+ @NonNull List<Integer> conflicts) {
+ List<Integer> configuredSlots = config.getOrderedMappedSlots();
+ for (int i = 0; i < configuredSlots.size() && placedButtons.size() < numUiButtons; ++i) {
+ int slotNumber = configuredSlots.get(i);
+ List<Integer> potentialButtons = config.getButtonsForSlot(slotNumber);
+ Collections.sort(potentialButtons, config.getSlotComparator());
+ for (int j = 0; j < potentialButtons.size(); ++j) {
+ if (allowedButtons.contains(potentialButtons.get(j))) {
+ placedButtons.add(potentialButtons.get(j));
+ conflicts.addAll(potentialButtons.subList(j + 1, potentialButtons.size()));
+ break;
+ }
+ }
+ }
+ }
+
+ private void placeConflictsInOpenSlots(
+ int numUiButtons,
+ @NonNull Set<Integer> allowedButtons,
+ @NonNull Set<Integer> disabledButtons,
+ @NonNull List<Integer> placedButtons,
+ @NonNull List<Integer> conflicts) {
+ Collections.sort(conflicts, config.getConflictComparator());
+ for (Integer conflict : conflicts) {
+ if (placedButtons.size() >= numUiButtons) {
+ return;
+ }
+ // If the conflict button is allowed but disabled, don't place it since it probably will
+ // move when it's enabled.
+ if (!allowedButtons.contains(conflict) || disabledButtons.contains(conflict)) {
+ continue;
+ }
+ placedButtons.add(conflict);
+ }
+ }
+}
diff --git a/java/com/android/incallui/incall/impl/ButtonChooserFactory.java b/java/com/android/incallui/incall/impl/ButtonChooserFactory.java
new file mode 100644
index 000000000..1b168a6f7
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/ButtonChooserFactory.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.incall.impl;
+
+import android.support.v4.util.ArrayMap;
+import android.telephony.TelephonyManager;
+import com.android.incallui.incall.impl.MappedButtonConfig.MappingInfo;
+import com.android.incallui.incall.protocol.InCallButtonIds;
+import java.util.Map;
+
+/**
+ * Creates {@link ButtonChooser} objects, based on the current network and phone type.
+ */
+class ButtonChooserFactory {
+
+ /**
+ * Creates the appropriate {@link ButtonChooser} based on the given information.
+ *
+ * @param voiceNetworkType the result of a call to {@link TelephonyManager#getVoiceNetworkType()}.
+ * @param isWiFi {@code true} if the call is made over WiFi, {@code false} otherwise.
+ * @param phoneType the result of a call to {@link TelephonyManager#getPhoneType()}.
+ * @return the ButtonChooser.
+ */
+ public static ButtonChooser newButtonChooser(
+ int voiceNetworkType, boolean isWiFi, int phoneType) {
+ if (voiceNetworkType == TelephonyManager.NETWORK_TYPE_LTE || isWiFi) {
+ return newImsAndWiFiButtonChooser();
+ }
+
+ if (phoneType == TelephonyManager.PHONE_TYPE_CDMA) {
+ return newCdmaButtonChooser();
+ }
+
+ if (phoneType == TelephonyManager.PHONE_TYPE_GSM) {
+ return newGsmButtonChooser();
+ }
+
+ return newImsAndWiFiButtonChooser();
+ }
+
+ private static ButtonChooser newImsAndWiFiButtonChooser() {
+ Map<Integer, MappingInfo> mapping = createCommonMapping();
+ mapping.put(
+ InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE,
+ MappingInfo.builder(4).setSlotOrder(0).build());
+ mapping.put(
+ InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO, MappingInfo.builder(4).setSlotOrder(10).build());
+ mapping.put(
+ InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY, MappingInfo.builder(5).setSlotOrder(0).build());
+ mapping.put(InCallButtonIds.BUTTON_HOLD, MappingInfo.builder(5).setSlotOrder(10).build());
+
+ return new ButtonChooser(new MappedButtonConfig(mapping));
+ }
+
+ private static ButtonChooser newCdmaButtonChooser() {
+ Map<Integer, MappingInfo> mapping = createCommonMapping();
+ mapping.put(
+ InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE,
+ MappingInfo.builder(4).setSlotOrder(0).build());
+ mapping.put(InCallButtonIds.BUTTON_SWAP, MappingInfo.builder(5).setSlotOrder(0).build());
+
+ return new ButtonChooser(new MappedButtonConfig(mapping));
+ }
+
+ private static ButtonChooser newGsmButtonChooser() {
+ Map<Integer, MappingInfo> mapping = createCommonMapping();
+ mapping.put(
+ InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY, MappingInfo.builder(4).setSlotOrder(0).build());
+ mapping.put(
+ InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE,
+ MappingInfo.builder(4).setSlotOrder(10).build());
+ mapping.put(InCallButtonIds.BUTTON_HOLD, MappingInfo.builder(5).setSlotOrder(0).build());
+
+ return new ButtonChooser(new MappedButtonConfig(mapping));
+ }
+
+ private static Map<Integer, MappingInfo> createCommonMapping() {
+ Map<Integer, MappingInfo> mapping = new ArrayMap<>();
+ mapping.put(InCallButtonIds.BUTTON_MUTE, MappingInfo.builder(0).build());
+ mapping.put(InCallButtonIds.BUTTON_DIALPAD, MappingInfo.builder(1).build());
+ mapping.put(InCallButtonIds.BUTTON_AUDIO, MappingInfo.builder(2).build());
+ mapping.put(InCallButtonIds.BUTTON_MERGE, MappingInfo.builder(3).setSlotOrder(0).build());
+ mapping.put(InCallButtonIds.BUTTON_ADD_CALL, MappingInfo.builder(3).build());
+ return mapping;
+ }
+}
diff --git a/java/com/android/incallui/incall/impl/ButtonController.java b/java/com/android/incallui/incall/impl/ButtonController.java
new file mode 100644
index 000000000..95a38be44
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/ButtonController.java
@@ -0,0 +1,584 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.incall.impl;
+
+import android.support.annotation.CallSuper;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.StringRes;
+import android.telecom.CallAudioState;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.View.OnClickListener;
+import com.android.dialer.common.Assert;
+import com.android.incallui.incall.impl.CheckableLabeledButton.OnCheckedChangeListener;
+import com.android.incallui.incall.protocol.InCallButtonIds;
+import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
+import com.android.incallui.incall.protocol.InCallScreenDelegate;
+
+/** Manages a single button. */
+interface ButtonController {
+
+ boolean isEnabled();
+
+ void setEnabled(boolean isEnabled);
+
+ boolean isAllowed();
+
+ void setAllowed(boolean isAllowed);
+
+ void setChecked(boolean isChecked);
+
+ @InCallButtonIds
+ int getInCallButtonId();
+
+ void setButton(CheckableLabeledButton button);
+
+ final class Controllers {
+
+ private static void resetButton(CheckableLabeledButton button) {
+ if (button != null) {
+ button.setOnCheckedChangeListener(null);
+ button.setOnClickListener(null);
+ }
+ }
+ }
+
+ abstract class CheckableButtonController implements ButtonController, OnCheckedChangeListener {
+
+ @NonNull protected final InCallButtonUiDelegate delegate;
+ @InCallButtonIds protected final int buttonId;
+ @StringRes protected final int checkedDescription;
+ @StringRes protected final int uncheckedDescription;
+ protected boolean isEnabled;
+ protected boolean isAllowed;
+ protected boolean isChecked;
+ protected CheckableLabeledButton button;
+
+ protected CheckableButtonController(
+ @NonNull InCallButtonUiDelegate delegate,
+ @InCallButtonIds int buttonId,
+ @StringRes int checkedContentDescription,
+ @StringRes int uncheckedContentDescription) {
+ Assert.isNotNull(delegate);
+ this.delegate = delegate;
+ this.buttonId = buttonId;
+ this.checkedDescription = checkedContentDescription;
+ this.uncheckedDescription = uncheckedContentDescription;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return isEnabled;
+ }
+
+ @Override
+ public void setEnabled(boolean isEnabled) {
+ this.isEnabled = isEnabled;
+ if (button != null) {
+ button.setEnabled(isEnabled);
+ }
+ }
+
+ @Override
+ public boolean isAllowed() {
+ return isAllowed;
+ }
+
+ @Override
+ public void setAllowed(boolean isAllowed) {
+ this.isAllowed = isAllowed;
+ if (button != null) {
+ button.setVisibility(isAllowed ? View.VISIBLE : View.INVISIBLE);
+ }
+ }
+
+ @Override
+ public void setChecked(boolean isChecked) {
+ this.isChecked = isChecked;
+ if (button != null) {
+ button.setChecked(isChecked);
+ }
+ }
+
+ @Override
+ @InCallButtonIds
+ public int getInCallButtonId() {
+ return buttonId;
+ }
+
+ @Override
+ @CallSuper
+ public void setButton(CheckableLabeledButton button) {
+ Controllers.resetButton(this.button);
+
+ this.button = button;
+ if (button != null) {
+ button.setEnabled(isEnabled);
+ button.setVisibility(isAllowed ? View.VISIBLE : View.INVISIBLE);
+ button.setChecked(isChecked);
+ button.setOnClickListener(null);
+ button.setOnCheckedChangeListener(this);
+ button.setContentDescription(
+ button.getContext().getText(isChecked ? checkedDescription : uncheckedDescription));
+ button.setShouldShowMoreIndicator(false);
+ }
+ }
+
+ @Override
+ public void onCheckedChanged(CheckableLabeledButton checkableLabeledButton, boolean isChecked) {
+ button.setContentDescription(
+ button.getContext().getText(isChecked ? checkedDescription : uncheckedDescription));
+ doCheckedChanged(isChecked);
+ }
+
+ protected abstract void doCheckedChanged(boolean isChecked);
+ }
+
+ abstract class SimpleCheckableButtonController extends CheckableButtonController {
+
+ @StringRes private final int label;
+ @DrawableRes private final int icon;
+
+ protected SimpleCheckableButtonController(
+ @NonNull InCallButtonUiDelegate delegate,
+ @InCallButtonIds int buttonId,
+ @StringRes int checkedContentDescription,
+ @StringRes int uncheckedContentDescription,
+ @StringRes int label,
+ @DrawableRes int icon) {
+ super(
+ delegate,
+ buttonId,
+ checkedContentDescription == 0 ? label : checkedContentDescription,
+ uncheckedContentDescription == 0 ? label : uncheckedContentDescription);
+ this.label = label;
+ this.icon = icon;
+ }
+
+ @Override
+ @CallSuper
+ public void setButton(CheckableLabeledButton button) {
+ super.setButton(button);
+ if (button != null) {
+ button.setLabelText(label);
+ button.setIconDrawable(icon);
+ }
+ }
+ }
+
+ abstract class NonCheckableButtonController implements ButtonController, OnClickListener {
+
+ protected final InCallButtonUiDelegate delegate;
+ @InCallButtonIds protected final int buttonId;
+ @StringRes protected final int contentDescription;
+ protected boolean isEnabled;
+ protected boolean isAllowed;
+ protected CheckableLabeledButton button;
+
+ protected NonCheckableButtonController(
+ InCallButtonUiDelegate delegate,
+ @InCallButtonIds int buttonId,
+ @StringRes int contentDescription) {
+ this.delegate = delegate;
+ this.buttonId = buttonId;
+ this.contentDescription = contentDescription;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return isEnabled;
+ }
+
+ @Override
+ public void setEnabled(boolean isEnabled) {
+ this.isEnabled = isEnabled;
+ if (button != null) {
+ button.setEnabled(isEnabled);
+ }
+ }
+
+ @Override
+ public boolean isAllowed() {
+ return isAllowed;
+ }
+
+ @Override
+ public void setAllowed(boolean isAllowed) {
+ this.isAllowed = isAllowed;
+ if (button != null) {
+ button.setVisibility(isAllowed ? View.VISIBLE : View.INVISIBLE);
+ }
+ }
+
+ @Override
+ public void setChecked(boolean isChecked) {
+ Assert.fail();
+ }
+
+ @Override
+ @InCallButtonIds
+ public int getInCallButtonId() {
+ return buttonId;
+ }
+
+ @Override
+ @CallSuper
+ public void setButton(CheckableLabeledButton button) {
+ Controllers.resetButton(this.button);
+
+ this.button = button;
+ if (button != null) {
+ button.setEnabled(isEnabled);
+ button.setVisibility(isAllowed ? View.VISIBLE : View.INVISIBLE);
+ button.setChecked(false);
+ button.setOnCheckedChangeListener(null);
+ button.setOnClickListener(this);
+ button.setContentDescription(button.getContext().getText(contentDescription));
+ button.setShouldShowMoreIndicator(false);
+ }
+ }
+ }
+
+ abstract class SimpleNonCheckableButtonController extends NonCheckableButtonController {
+
+ @StringRes private final int label;
+ @DrawableRes private final int icon;
+
+ protected SimpleNonCheckableButtonController(
+ InCallButtonUiDelegate delegate,
+ @InCallButtonIds int buttonId,
+ @StringRes int contentDescription,
+ @StringRes int label,
+ @DrawableRes int icon) {
+ super(delegate, buttonId, contentDescription == 0 ? label : contentDescription);
+ this.label = label;
+ this.icon = icon;
+ }
+
+ @Override
+ @CallSuper
+ public void setButton(CheckableLabeledButton button) {
+ super.setButton(button);
+ if (button != null) {
+ button.setLabelText(label);
+ button.setIconDrawable(icon);
+ }
+ }
+ }
+
+ class MuteButtonController extends SimpleCheckableButtonController {
+
+ public MuteButtonController(InCallButtonUiDelegate delegate) {
+ super(
+ delegate,
+ InCallButtonIds.BUTTON_MUTE,
+ R.string.incall_content_description_muted,
+ R.string.incall_content_description_unmuted,
+ R.string.incall_label_mute,
+ R.drawable.quantum_ic_mic_off_white_36);
+ }
+
+ @Override
+ public void doCheckedChanged(boolean isChecked) {
+ delegate.muteClicked(isChecked);
+ }
+ }
+
+ class SpeakerButtonController
+ implements ButtonController, OnCheckedChangeListener, OnClickListener {
+
+ @NonNull private final InCallButtonUiDelegate delegate;
+ private boolean isEnabled;
+ private boolean isAllowed;
+ private boolean isChecked;
+ private CheckableLabeledButton button;
+
+ @StringRes private int label = R.string.incall_label_speaker;
+ @DrawableRes private int icon = R.drawable.quantum_ic_volume_up_white_36;
+ private boolean checkable;
+ private CharSequence contentDescription;
+ private CharSequence checkedContentDescription;
+ private CharSequence uncheckedContentDescription;
+
+ public SpeakerButtonController(@NonNull InCallButtonUiDelegate delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return isEnabled;
+ }
+
+ @Override
+ public void setEnabled(boolean isEnabled) {
+ this.isEnabled = isEnabled;
+ if (button != null) {
+ button.setEnabled(isEnabled && isAllowed);
+ }
+ }
+
+ @Override
+ public boolean isAllowed() {
+ return isAllowed;
+ }
+
+ @Override
+ public void setAllowed(boolean isAllowed) {
+ this.isAllowed = isAllowed;
+ if (button != null) {
+ button.setEnabled(isEnabled && isAllowed);
+ }
+ }
+
+ @Override
+ public void setChecked(boolean isChecked) {
+ this.isChecked = isChecked;
+ if (button != null) {
+ button.setChecked(isChecked);
+ }
+ }
+
+ @Override
+ public int getInCallButtonId() {
+ return InCallButtonIds.BUTTON_AUDIO;
+ }
+
+ @Override
+ public void setButton(CheckableLabeledButton button) {
+ this.button = button;
+ if (button != null) {
+ button.setEnabled(isEnabled && isAllowed);
+ button.setVisibility(View.VISIBLE);
+ button.setChecked(isChecked);
+ button.setOnClickListener(checkable ? null : this);
+ button.setOnCheckedChangeListener(checkable ? this : null);
+ button.setLabelText(label);
+ button.setIconDrawable(icon);
+ button.setContentDescription(
+ isChecked ? checkedContentDescription : uncheckedContentDescription);
+ button.setShouldShowMoreIndicator(!checkable);
+ }
+ }
+
+ public void setAudioState(CallAudioState audioState) {
+ @StringRes int contentDescriptionResId;
+ if ((audioState.getSupportedRouteMask() & CallAudioState.ROUTE_BLUETOOTH)
+ == CallAudioState.ROUTE_BLUETOOTH) {
+ checkable = false;
+ isChecked = false;
+ label = R.string.incall_label_audio;
+
+ if ((audioState.getRoute() & CallAudioState.ROUTE_BLUETOOTH)
+ == CallAudioState.ROUTE_BLUETOOTH) {
+ icon = R.drawable.quantum_ic_bluetooth_audio_white_36;
+ contentDescriptionResId = R.string.incall_content_description_bluetooth;
+ } else if ((audioState.getRoute() & CallAudioState.ROUTE_SPEAKER)
+ == CallAudioState.ROUTE_SPEAKER) {
+ icon = R.drawable.quantum_ic_volume_up_white_36;
+ contentDescriptionResId = R.string.incall_content_description_speaker;
+ } else if ((audioState.getRoute() & CallAudioState.ROUTE_WIRED_HEADSET)
+ == CallAudioState.ROUTE_WIRED_HEADSET) {
+ icon = R.drawable.quantum_ic_headset_white_36;
+ contentDescriptionResId = R.string.incall_content_description_headset;
+ } else {
+ icon = R.drawable.ic_phone_audio_white_36dp;
+ contentDescriptionResId = R.string.incall_content_description_earpiece;
+ }
+ } else {
+ checkable = true;
+ isChecked = audioState.getRoute() == CallAudioState.ROUTE_SPEAKER;
+ label = R.string.incall_label_speaker;
+ icon = R.drawable.quantum_ic_volume_up_white_36;
+ contentDescriptionResId = R.string.incall_content_description_speaker;
+ }
+
+ contentDescription = delegate.getContext().getText(contentDescriptionResId);
+ checkedContentDescription =
+ TextUtils.concat(
+ contentDescription,
+ delegate.getContext().getText(R.string.incall_talkback_speaker_on));
+ uncheckedContentDescription =
+ TextUtils.concat(
+ contentDescription,
+ delegate.getContext().getText(R.string.incall_talkback_speaker_off));
+ setButton(button);
+ }
+
+ @Override
+ public void onClick(View v) {
+ delegate.showAudioRouteSelector();
+ }
+
+ @Override
+ public void onCheckedChanged(CheckableLabeledButton checkableLabeledButton, boolean isChecked) {
+ checkableLabeledButton.setContentDescription(
+ isChecked ? checkedContentDescription : uncheckedContentDescription);
+ delegate.toggleSpeakerphone();
+ }
+ }
+
+ class DialpadButtonController extends SimpleCheckableButtonController {
+
+ public DialpadButtonController(@NonNull InCallButtonUiDelegate delegate) {
+ super(
+ delegate,
+ InCallButtonIds.BUTTON_DIALPAD,
+ 0,
+ 0,
+ R.string.incall_label_dialpad,
+ R.drawable.quantum_ic_dialpad_white_36);
+ }
+
+ @Override
+ public void doCheckedChanged(boolean isChecked) {
+ delegate.showDialpadClicked(isChecked);
+ }
+ }
+
+ class HoldButtonController extends SimpleCheckableButtonController {
+
+ public HoldButtonController(@NonNull InCallButtonUiDelegate delegate) {
+ super(
+ delegate,
+ InCallButtonIds.BUTTON_HOLD,
+ R.string.incall_content_description_unhold,
+ R.string.incall_content_description_hold,
+ R.string.incall_label_hold,
+ R.drawable.quantum_ic_pause_white_36);
+ }
+
+ @Override
+ public void doCheckedChanged(boolean isChecked) {
+ delegate.holdClicked(isChecked);
+ }
+ }
+
+ class AddCallButtonController extends SimpleNonCheckableButtonController {
+
+ public AddCallButtonController(@NonNull InCallButtonUiDelegate delegate) {
+ super(
+ delegate,
+ InCallButtonIds.BUTTON_ADD_CALL,
+ 0,
+ R.string.incall_label_add_call,
+ R.drawable.ic_addcall_white);
+ Assert.isNotNull(delegate);
+ }
+
+ @Override
+ public void onClick(View view) {
+ delegate.addCallClicked();
+ }
+ }
+
+ class SwapButtonController extends SimpleNonCheckableButtonController {
+
+ public SwapButtonController(@NonNull InCallButtonUiDelegate delegate) {
+ super(
+ delegate,
+ InCallButtonIds.BUTTON_SWAP,
+ R.string.incall_content_description_swap_calls,
+ R.string.incall_label_swap,
+ R.drawable.quantum_ic_swap_calls_white_36);
+ Assert.isNotNull(delegate);
+ }
+
+ @Override
+ public void onClick(View view) {
+ delegate.swapClicked();
+ }
+ }
+
+ class MergeButtonController extends SimpleNonCheckableButtonController {
+
+ public MergeButtonController(@NonNull InCallButtonUiDelegate delegate) {
+ super(
+ delegate,
+ InCallButtonIds.BUTTON_MERGE,
+ R.string.incall_content_description_merge_calls,
+ R.string.incall_label_merge,
+ R.drawable.quantum_ic_call_merge_white_36);
+ Assert.isNotNull(delegate);
+ }
+
+ @Override
+ public void onClick(View view) {
+ delegate.mergeClicked();
+ }
+ }
+
+ class UpgradeToVideoButtonController extends SimpleNonCheckableButtonController {
+
+ public UpgradeToVideoButtonController(@NonNull InCallButtonUiDelegate delegate) {
+ super(
+ delegate,
+ InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO,
+ 0,
+ R.string.incall_label_videocall,
+ R.drawable.quantum_ic_videocam_white_36);
+ Assert.isNotNull(delegate);
+ }
+
+ @Override
+ public void onClick(View view) {
+ delegate.changeToVideoClicked();
+ }
+ }
+
+ class ManageConferenceButtonController extends SimpleNonCheckableButtonController {
+
+ private final InCallScreenDelegate inCallScreenDelegate;
+
+ public ManageConferenceButtonController(@NonNull InCallScreenDelegate inCallScreenDelegate) {
+ super(
+ null,
+ InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE,
+ R.string.a11y_description_incall_label_manage_content,
+ R.string.incall_label_manage,
+ R.drawable.quantum_ic_group_white_36);
+ Assert.isNotNull(inCallScreenDelegate);
+ this.inCallScreenDelegate = inCallScreenDelegate;
+ }
+
+ @Override
+ public void onClick(View view) {
+ inCallScreenDelegate.onManageConferenceClicked();
+ }
+ }
+
+ class SwitchToSecondaryButtonController extends SimpleNonCheckableButtonController {
+
+ private final InCallScreenDelegate inCallScreenDelegate;
+
+ public SwitchToSecondaryButtonController(InCallScreenDelegate inCallScreenDelegate) {
+ super(
+ null,
+ InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY,
+ R.string.incall_content_description_swap_calls,
+ R.string.incall_label_swap,
+ R.drawable.quantum_ic_swap_calls_white_36);
+ Assert.isNotNull(inCallScreenDelegate);
+ this.inCallScreenDelegate = inCallScreenDelegate;
+ }
+
+ @Override
+ public void onClick(View view) {
+ inCallScreenDelegate.onSecondaryInfoClicked();
+ }
+ }
+}
diff --git a/java/com/android/incallui/incall/impl/CheckableLabeledButton.java b/java/com/android/incallui/incall/impl/CheckableLabeledButton.java
new file mode 100644
index 000000000..a681adcb4
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/CheckableLabeledButton.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.incall.impl;
+
+import android.animation.AnimatorInflater;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.StringRes;
+import android.text.TextUtils.TruncateAt;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.SoundEffectConstants;
+import android.widget.Checkable;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/** A button to show on the incall screen */
+public class CheckableLabeledButton extends LinearLayout implements Checkable {
+
+ private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked};
+ private static final float DISABLED_STATE_OPACITY = .3f;
+ private boolean broadcasting;
+ private boolean isChecked;
+ private OnCheckedChangeListener onCheckedChangeListener;
+ private ImageView iconView;
+ private TextView labelView;
+ private Drawable background;
+ private Drawable backgroundMore;
+
+ public CheckableLabeledButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs);
+ }
+
+ public CheckableLabeledButton(Context context) {
+ this(context, null);
+ }
+
+ private void init(Context context, AttributeSet attrs) {
+ setOrientation(VERTICAL);
+ setGravity(Gravity.CENTER_HORIZONTAL);
+ Drawable icon;
+ CharSequence labelText;
+ boolean enabled;
+
+ backgroundMore = getResources().getDrawable(R.drawable.incall_button_background_more, null);
+ background = getResources().getDrawable(R.drawable.incall_button_background, null);
+
+ TypedArray typedArray =
+ context.obtainStyledAttributes(attrs, R.styleable.CheckableLabeledButton);
+ icon = typedArray.getDrawable(R.styleable.CheckableLabeledButton_incall_icon);
+ labelText = typedArray.getString(R.styleable.CheckableLabeledButton_incall_labelText);
+ enabled = typedArray.getBoolean(R.styleable.CheckableLabeledButton_android_enabled, true);
+ typedArray.recycle();
+
+ int paddingSize = getResources().getDimensionPixelOffset(R.dimen.incall_button_padding);
+ setPadding(paddingSize, paddingSize, paddingSize, paddingSize);
+
+ int iconSize = getResources().getDimensionPixelSize(R.dimen.incall_labeled_button_size);
+
+ iconView = new ImageView(context, null, android.R.style.Widget_Material_Button_Colored);
+ LayoutParams iconParams = generateDefaultLayoutParams();
+ iconParams.width = iconSize;
+ iconParams.height = iconSize;
+ iconView.setLayoutParams(iconParams);
+ iconView.setScaleType(ScaleType.CENTER_INSIDE);
+ iconView.setImageDrawable(icon);
+ iconView.setImageTintMode(Mode.SRC_IN);
+ iconView.setImageTintList(getResources().getColorStateList(R.color.incall_button_icon, null));
+ iconView.setBackground(getResources().getDrawable(R.drawable.incall_button_background, null));
+ iconView.setDuplicateParentStateEnabled(true);
+ iconView.setElevation(getResources().getDimension(R.dimen.incall_button_elevation));
+ iconView.setStateListAnimator(
+ AnimatorInflater.loadStateListAnimator(context, R.animator.incall_button_elevation));
+ addView(iconView);
+
+ labelView = new TextView(context);
+ LayoutParams labelParams = generateDefaultLayoutParams();
+ labelParams.width = LayoutParams.WRAP_CONTENT;
+ labelParams.height = LayoutParams.WRAP_CONTENT;
+ labelParams.topMargin =
+ context.getResources().getDimensionPixelOffset(R.dimen.incall_button_label_margin);
+ labelView.setLayoutParams(labelParams);
+ labelView.setTextAppearance(R.style.Dialer_Incall_TextAppearance_Label);
+ labelView.setText(labelText);
+ labelView.setSingleLine();
+ labelView.setMaxEms(9);
+ labelView.setEllipsize(TruncateAt.END);
+ labelView.setGravity(Gravity.CENTER);
+ labelView.setDuplicateParentStateEnabled(true);
+ addView(labelView);
+
+ setFocusable(true);
+ setClickable(true);
+ setEnabled(enabled);
+ setOutlineProvider(null);
+ }
+
+ @Override
+ public void refreshDrawableState() {
+ super.refreshDrawableState();
+ iconView.setAlpha(isEnabled() ? 1f : DISABLED_STATE_OPACITY);
+ labelView.setAlpha(isEnabled() ? 1f : DISABLED_STATE_OPACITY);
+ }
+
+ public void setIconDrawable(@DrawableRes int drawableRes) {
+ iconView.setImageResource(drawableRes);
+ }
+
+ public void setLabelText(@StringRes int stringRes) {
+ labelView.setText(stringRes);
+ }
+
+ /** Shows or hides a little down arrow to indicate that the button will pop up a menu. */
+ public void setShouldShowMoreIndicator(boolean shouldShow) {
+ iconView.setBackground(shouldShow ? backgroundMore : background);
+ }
+
+ @Override
+ public boolean isChecked() {
+ return isChecked;
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ performSetChecked(checked);
+ }
+
+ @Override
+ public void toggle() {
+ userRequestedSetChecked(!isChecked());
+ }
+
+ @Override
+ public int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+ if (isChecked()) {
+ mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+ }
+ return drawableState;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ invalidate();
+ }
+
+ public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
+ this.onCheckedChangeListener = listener;
+ }
+
+ @Override
+ public boolean performClick() {
+ if (!isCheckable()) {
+ return super.performClick();
+ }
+
+ toggle();
+ final boolean handled = super.performClick();
+ if (!handled) {
+ // View only makes a sound effect if the onClickListener was
+ // called, so we'll need to make one here instead.
+ playSoundEffect(SoundEffectConstants.CLICK);
+ }
+ return handled;
+ }
+
+ private boolean isCheckable() {
+ return onCheckedChangeListener != null;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ SavedState savedState = (SavedState) state;
+ super.onRestoreInstanceState(savedState.getSuperState());
+ performSetChecked(savedState.isChecked);
+ requestLayout();
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ return new SavedState(isChecked(), super.onSaveInstanceState());
+ }
+
+ /**
+ * Called when the state of the button should be updated, this should not be the result of user
+ * interaction.
+ *
+ * @param checked {@code true} if the button should be in the checked state, {@code false}
+ * otherwise.
+ */
+ private void performSetChecked(boolean checked) {
+ if (isChecked() == checked) {
+ return;
+ }
+ isChecked = checked;
+ refreshDrawableState();
+ }
+
+ /**
+ * Called when the user interacts with a button. This should not result in the button updating
+ * state, rather the request should be propagated to the associated listener.
+ *
+ * @param checked {@code true} if the button should be in the checked state, {@code false}
+ * otherwise.
+ */
+ private void userRequestedSetChecked(boolean checked) {
+ if (isChecked() == checked) {
+ return;
+ }
+ if (broadcasting) {
+ return;
+ }
+ broadcasting = true;
+ if (onCheckedChangeListener != null) {
+ onCheckedChangeListener.onCheckedChanged(this, checked);
+ }
+ broadcasting = false;
+ }
+
+ /** Callback interface to notify when the button's checked state has changed */
+ public interface OnCheckedChangeListener {
+
+ void onCheckedChanged(CheckableLabeledButton checkableLabeledButton, boolean isChecked);
+ }
+
+ private static class SavedState extends BaseSavedState {
+
+ public static final Creator<SavedState> CREATOR =
+ new Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ public final boolean isChecked;
+
+ private SavedState(boolean isChecked, Parcelable superState) {
+ super(superState);
+ this.isChecked = isChecked;
+ }
+
+ protected SavedState(Parcel in) {
+ super(in);
+ isChecked = in.readByte() != 0;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeByte((byte) (isChecked ? 1 : 0));
+ }
+ }
+}
diff --git a/java/com/android/incallui/incall/impl/InCallButtonGridFragment.java b/java/com/android/incallui/incall/impl/InCallButtonGridFragment.java
new file mode 100644
index 000000000..db0b5b9b8
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/InCallButtonGridFragment.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.incall.impl;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.util.ArraySet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.FragmentUtils;
+import com.android.incallui.incall.protocol.InCallButtonIds;
+import java.util.List;
+import java.util.Set;
+
+/** Fragment for the in call buttons (mute, speaker, ect.). */
+public class InCallButtonGridFragment extends Fragment {
+
+ private static final int BUTTON_COUNT = 6;
+ private static final int BUTTONS_PER_ROW = 3;
+
+ private CheckableLabeledButton[] buttons = new CheckableLabeledButton[BUTTON_COUNT];
+ private OnButtonGridCreatedListener buttonGridListener;
+
+ public static Fragment newInstance() {
+ return new InCallButtonGridFragment();
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle bundle) {
+ super.onCreate(bundle);
+ buttonGridListener = FragmentUtils.getParent(this, OnButtonGridCreatedListener.class);
+ Assert.isNotNull(buttonGridListener);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, @Nullable ViewGroup parent, @Nullable Bundle bundle) {
+ View view = inflater.inflate(R.layout.incall_button_grid, parent, false);
+
+ buttons[0] = ((CheckableLabeledButton) view.findViewById(R.id.incall_first_button));
+ buttons[1] = ((CheckableLabeledButton) view.findViewById(R.id.incall_second_button));
+ buttons[2] = ((CheckableLabeledButton) view.findViewById(R.id.incall_third_button));
+ buttons[3] = ((CheckableLabeledButton) view.findViewById(R.id.incall_fourth_button));
+ buttons[4] = ((CheckableLabeledButton) view.findViewById(R.id.incall_fifth_button));
+ buttons[5] = ((CheckableLabeledButton) view.findViewById(R.id.incall_sixth_button));
+
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle bundle) {
+ super.onViewCreated(view, bundle);
+ buttonGridListener.onButtonGridCreated(this);
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ buttonGridListener.onButtonGridDestroyed();
+ }
+
+ public void onInCallScreenDialpadVisibilityChange(boolean isShowing) {
+ for (CheckableLabeledButton button : buttons) {
+ button.setImportantForAccessibility(
+ isShowing
+ ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+ : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+ }
+ }
+
+ public int updateButtonStates(
+ List<ButtonController> buttonControllers,
+ @Nullable ButtonChooser buttonChooser,
+ int voiceNetworkType,
+ int phoneType) {
+ Set<Integer> allowedButtons = new ArraySet<>();
+ Set<Integer> disabledButtons = new ArraySet<>();
+ for (ButtonController controller : buttonControllers) {
+ if (controller.isAllowed()) {
+ allowedButtons.add(controller.getInCallButtonId());
+ if (!controller.isEnabled()) {
+ disabledButtons.add(controller.getInCallButtonId());
+ }
+ }
+ }
+
+ for (ButtonController controller : buttonControllers) {
+ controller.setButton(null);
+ }
+
+ if (buttonChooser == null) {
+ buttonChooser =
+ ButtonChooserFactory.newButtonChooser(voiceNetworkType, false /* isWiFi */, phoneType);
+ }
+
+ int numVisibleButtons = getResources().getInteger(R.integer.incall_num_rows) * BUTTONS_PER_ROW;
+ List<Integer> buttonsToPlace =
+ buttonChooser.getButtonPlacement(numVisibleButtons, allowedButtons, disabledButtons);
+
+ for (int i = 0; i < BUTTON_COUNT; ++i) {
+ if (i >= buttonsToPlace.size()) {
+ buttons[i].setVisibility(View.INVISIBLE);
+ continue;
+ }
+ @InCallButtonIds int button = buttonsToPlace.get(i);
+ buttonGridListener.getButtonController(button).setButton(buttons[i]);
+ }
+
+ return numVisibleButtons;
+ }
+
+ /** Interface to let the listener know the status of the button grid. */
+ public interface OnButtonGridCreatedListener {
+ void onButtonGridCreated(InCallButtonGridFragment inCallButtonGridFragment);
+ void onButtonGridDestroyed();
+
+ ButtonController getButtonController(@InCallButtonIds int id);
+ }
+}
diff --git a/java/com/android/incallui/incall/impl/InCallFragment.java b/java/com/android/incallui/incall/impl/InCallFragment.java
new file mode 100644
index 000000000..ef8a1edd8
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/InCallFragment.java
@@ -0,0 +1,501 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.incall.impl;
+
+import android.Manifest.permission;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.design.widget.TabLayout;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.view.ViewPager;
+import android.telecom.CallAudioState;
+import android.telephony.TelephonyManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.Toast;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.FragmentUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.multimedia.MultimediaData;
+import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment;
+import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRouteSelectorPresenter;
+import com.android.incallui.contactgrid.ContactGridManager;
+import com.android.incallui.hold.OnHoldFragment;
+import com.android.incallui.incall.impl.ButtonController.SpeakerButtonController;
+import com.android.incallui.incall.impl.InCallButtonGridFragment.OnButtonGridCreatedListener;
+import com.android.incallui.incall.protocol.InCallButtonIds;
+import com.android.incallui.incall.protocol.InCallButtonIdsExtension;
+import com.android.incallui.incall.protocol.InCallButtonUi;
+import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
+import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory;
+import com.android.incallui.incall.protocol.InCallScreen;
+import com.android.incallui.incall.protocol.InCallScreenDelegate;
+import com.android.incallui.incall.protocol.InCallScreenDelegateFactory;
+import com.android.incallui.incall.protocol.PrimaryCallState;
+import com.android.incallui.incall.protocol.PrimaryInfo;
+import com.android.incallui.incall.protocol.SecondaryInfo;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Fragment that shows UI for an ongoing voice call. */
+public class InCallFragment extends Fragment
+ implements InCallScreen,
+ InCallButtonUi,
+ OnClickListener,
+ AudioRouteSelectorPresenter,
+ OnButtonGridCreatedListener {
+
+ private List<ButtonController> buttonControllers = new ArrayList<>();
+ private View endCallButton;
+ private TabLayout tabLayout;
+ private ViewPager pager;
+ private InCallPagerAdapter adapter;
+ private ContactGridManager contactGridManager;
+ private InCallScreenDelegate inCallScreenDelegate;
+ private InCallButtonUiDelegate inCallButtonUiDelegate;
+ private InCallButtonGridFragment inCallButtonGridFragment;
+ @Nullable private ButtonChooser buttonChooser;
+ private SecondaryInfo savedSecondaryInfo;
+ private int voiceNetworkType;
+ private int phoneType;
+ private boolean stateRestored;
+
+ private static boolean isSupportedButton(@InCallButtonIds int id) {
+ return id == InCallButtonIds.BUTTON_AUDIO
+ || id == InCallButtonIds.BUTTON_MUTE
+ || id == InCallButtonIds.BUTTON_DIALPAD
+ || id == InCallButtonIds.BUTTON_HOLD
+ || id == InCallButtonIds.BUTTON_SWAP
+ || id == InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO
+ || id == InCallButtonIds.BUTTON_ADD_CALL
+ || id == InCallButtonIds.BUTTON_MERGE
+ || id == InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (savedSecondaryInfo != null) {
+ setSecondary(savedSecondaryInfo);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ inCallButtonUiDelegate =
+ FragmentUtils.getParent(this, InCallButtonUiDelegateFactory.class)
+ .newInCallButtonUiDelegate();
+ if (savedInstanceState != null) {
+ inCallButtonUiDelegate.onRestoreInstanceState(savedInstanceState);
+ stateRestored = true;
+ }
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ @NonNull LayoutInflater layoutInflater,
+ @Nullable ViewGroup viewGroup,
+ @Nullable Bundle bundle) {
+ LogUtil.i("InCallFragment.onCreateView", null);
+ final View view = layoutInflater.inflate(R.layout.frag_incall_voice, viewGroup, false);
+ contactGridManager =
+ new ContactGridManager(
+ view,
+ (ImageView) view.findViewById(R.id.contactgrid_avatar),
+ getResources().getDimensionPixelSize(R.dimen.incall_avatar_size),
+ true /* showAnonymousAvatar */);
+
+ tabLayout = (TabLayout) view.findViewById(R.id.incall_tab_dots);
+ pager = (ViewPager) view.findViewById(R.id.incall_pager);
+
+ endCallButton = view.findViewById(R.id.incall_end_call);
+ endCallButton.setOnClickListener(this);
+
+ if (ContextCompat.checkSelfPermission(getContext(), permission.READ_PHONE_STATE)
+ != PackageManager.PERMISSION_GRANTED) {
+ voiceNetworkType = TelephonyManager.NETWORK_TYPE_UNKNOWN;
+ } else {
+
+ voiceNetworkType =
+ VERSION.SDK_INT >= VERSION_CODES.N
+ ? getContext().getSystemService(TelephonyManager.class).getVoiceNetworkType()
+ : TelephonyManager.NETWORK_TYPE_UNKNOWN;
+ }
+ phoneType = getContext().getSystemService(TelephonyManager.class).getPhoneType();
+ return view;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ inCallButtonUiDelegate.refreshMuteState();
+ inCallScreenDelegate.onInCallScreenResumed();
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle bundle) {
+ LogUtil.i("InCallFragment.onViewCreated", null);
+ super.onViewCreated(view, bundle);
+ inCallScreenDelegate =
+ FragmentUtils.getParent(this, InCallScreenDelegateFactory.class).newInCallScreenDelegate();
+ Assert.isNotNull(inCallScreenDelegate);
+
+ buttonControllers.add(new ButtonController.MuteButtonController(inCallButtonUiDelegate));
+ buttonControllers.add(new ButtonController.SpeakerButtonController(inCallButtonUiDelegate));
+ buttonControllers.add(new ButtonController.DialpadButtonController(inCallButtonUiDelegate));
+ buttonControllers.add(new ButtonController.HoldButtonController(inCallButtonUiDelegate));
+ buttonControllers.add(new ButtonController.AddCallButtonController(inCallButtonUiDelegate));
+ buttonControllers.add(new ButtonController.SwapButtonController(inCallButtonUiDelegate));
+ buttonControllers.add(new ButtonController.MergeButtonController(inCallButtonUiDelegate));
+ buttonControllers.add(
+ new ButtonController.UpgradeToVideoButtonController(inCallButtonUiDelegate));
+ buttonControllers.add(
+ new ButtonController.ManageConferenceButtonController(inCallScreenDelegate));
+ buttonControllers.add(
+ new ButtonController.SwitchToSecondaryButtonController(inCallScreenDelegate));
+
+ inCallScreenDelegate.onInCallScreenDelegateInit(this);
+ inCallScreenDelegate.onInCallScreenReady();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ inCallScreenDelegate.onInCallScreenUnready();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ inCallButtonUiDelegate.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view == endCallButton) {
+ LogUtil.i("InCallFragment.onClick", "end call button clicked");
+ inCallScreenDelegate.onEndCallClicked();
+ } else {
+ LogUtil.e("InCallFragment.onClick", "unknown view: " + view);
+ Assert.fail();
+ }
+ }
+
+ @Override
+ public void setPrimary(@NonNull PrimaryInfo primaryInfo) {
+ LogUtil.i("InCallFragment.setPrimary", primaryInfo.toString());
+ if (adapter == null) {
+ initAdapter(primaryInfo.multimediaData);
+ }
+ contactGridManager.setPrimary(primaryInfo);
+
+ if (primaryInfo.shouldShowLocation) {
+ // Hide the avatar to make room for location
+ contactGridManager.setAvatarHidden(true);
+
+ // Need to widen the contact grid to fit location information
+ View contactGridView = getView().findViewById(R.id.incall_contact_grid);
+ ViewGroup.LayoutParams params = contactGridView.getLayoutParams();
+ if (params instanceof ViewGroup.MarginLayoutParams) {
+ ((ViewGroup.MarginLayoutParams) params).setMarginStart(0);
+ ((ViewGroup.MarginLayoutParams) params).setMarginEnd(0);
+ }
+ contactGridView.setLayoutParams(params);
+
+ // Need to let the dialpad move up a little further when location info is being shown
+ View dialpadView = getView().findViewById(R.id.incall_dialpad_container);
+ params = dialpadView.getLayoutParams();
+ if (params instanceof RelativeLayout.LayoutParams) {
+ ((RelativeLayout.LayoutParams) params).removeRule(RelativeLayout.BELOW);
+ }
+ dialpadView.setLayoutParams(params);
+ }
+ }
+
+ private void initAdapter(MultimediaData multimediaData) {
+ adapter = new InCallPagerAdapter(getChildFragmentManager(), multimediaData);
+ pager.setAdapter(adapter);
+
+ if (adapter.getCount() > 1) {
+ tabLayout.setVisibility(pager.getVisibility());
+ tabLayout.setupWithViewPager(pager, true);
+ if (!stateRestored) {
+ new Handler()
+ .postDelayed(
+ new Runnable() {
+ @Override
+ public void run() {
+ // In order to prevent user confusion and educate the user on our UI, we animate
+ // the view pager to the button grid after 2 seconds show them when the UI is
+ // that they are more familiar with.
+ pager.setCurrentItem(adapter.getButtonGridPosition());
+ }
+ },
+ 2000);
+ }
+ } else {
+ tabLayout.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void setSecondary(@NonNull SecondaryInfo secondaryInfo) {
+ LogUtil.i("InCallFragment.setSecondary", secondaryInfo.toString());
+ getButtonController(InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY)
+ .setEnabled(secondaryInfo.shouldShow);
+ getButtonController(InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY)
+ .setAllowed(secondaryInfo.shouldShow);
+ updateButtonStates();
+
+ if (!isAdded()) {
+ savedSecondaryInfo = secondaryInfo;
+ return;
+ }
+ savedSecondaryInfo = null;
+ FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
+ Fragment oldBanner = getChildFragmentManager().findFragmentById(R.id.incall_on_hold_banner);
+ if (secondaryInfo.shouldShow) {
+ transaction.replace(R.id.incall_on_hold_banner, OnHoldFragment.newInstance(secondaryInfo));
+ } else {
+ if (oldBanner != null) {
+ transaction.remove(oldBanner);
+ }
+ }
+ transaction.setCustomAnimations(R.anim.abc_slide_in_top, R.anim.abc_slide_out_top);
+ transaction.commitAllowingStateLoss();
+ }
+
+ @Override
+ public void setCallState(@NonNull PrimaryCallState primaryCallState) {
+ LogUtil.i("InCallFragment.setCallState", primaryCallState.toString());
+ contactGridManager.setCallState(primaryCallState);
+ buttonChooser =
+ ButtonChooserFactory.newButtonChooser(voiceNetworkType, primaryCallState.isWifi, phoneType);
+ updateButtonStates();
+ }
+
+ @Override
+ public void setEndCallButtonEnabled(boolean enabled, boolean animate) {
+ if (endCallButton != null) {
+ endCallButton.setEnabled(enabled);
+ }
+ }
+
+ @Override
+ public void showManageConferenceCallButton(boolean visible) {
+ getButtonController(InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE).setAllowed(visible);
+ getButtonController(InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE).setEnabled(visible);
+ updateButtonStates();
+ }
+
+ @Override
+ public boolean isManageConferenceVisible() {
+ return getButtonController(InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE).isAllowed();
+ }
+
+ @Override
+ public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ contactGridManager.dispatchPopulateAccessibilityEvent(event);
+ }
+
+ @Override
+ public void showNoteSentToast() {
+ LogUtil.i("InCallFragment.showNoteSentToast", null);
+ Toast.makeText(getContext(), R.string.incall_note_sent, Toast.LENGTH_LONG).show();
+ }
+
+ @Override
+ public void updateInCallScreenColors() {}
+
+ @Override
+ public void onInCallScreenDialpadVisibilityChange(boolean isShowing) {
+ LogUtil.i("InCallFragment.onInCallScreenDialpadVisibilityChange", "isShowing: " + isShowing);
+ // Take note that the dialpad button isShowing
+ getButtonController(InCallButtonIds.BUTTON_DIALPAD).setChecked(isShowing);
+
+ // This check is needed because there is a race condition where we attempt to update
+ // ButtonGridFragment before it is ready, so we check whether it is ready first and once it is
+ // ready, #onButtonGridCreated will mark the dialpad button as isShowing.
+ if (inCallButtonGridFragment != null) {
+ // Update the Android Button's state to isShowing.
+ inCallButtonGridFragment.onInCallScreenDialpadVisibilityChange(isShowing);
+ }
+ }
+
+ @Override
+ public int getAnswerAndDialpadContainerResourceId() {
+ return R.id.incall_dialpad_container;
+ }
+
+ @Override
+ public Fragment getInCallScreenFragment() {
+ return this;
+ }
+
+ @Override
+ public void showButton(@InCallButtonIds int buttonId, boolean show) {
+ LogUtil.v(
+ "InCallFragment.showButton",
+ "buttionId: %s, show: %b",
+ InCallButtonIdsExtension.toString(buttonId),
+ show);
+ if (isSupportedButton(buttonId)) {
+ getButtonController(buttonId).setAllowed(show);
+ }
+ }
+
+ @Override
+ public void enableButton(@InCallButtonIds int buttonId, boolean enable) {
+ LogUtil.v(
+ "InCallFragment.enableButton",
+ "buttonId: %s, enable: %b",
+ InCallButtonIdsExtension.toString(buttonId),
+ enable);
+ if (isSupportedButton(buttonId)) {
+ getButtonController(buttonId).setEnabled(enable);
+ }
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ LogUtil.v("InCallFragment.setEnabled", "enabled: " + enabled);
+ for (ButtonController buttonController : buttonControllers) {
+ buttonController.setEnabled(enabled);
+ }
+ }
+
+ @Override
+ public void setHold(boolean value) {
+ getButtonController(InCallButtonIds.BUTTON_HOLD).setChecked(value);
+ }
+
+ @Override
+ public void setCameraSwitched(boolean isBackFacingCamera) {}
+
+ @Override
+ public void setVideoPaused(boolean isPaused) {}
+
+ @Override
+ public void setAudioState(CallAudioState audioState) {
+ LogUtil.i("InCallFragment.setAudioState", "audioState: " + audioState);
+ ((SpeakerButtonController) getButtonController(InCallButtonIds.BUTTON_AUDIO))
+ .setAudioState(audioState);
+ getButtonController(InCallButtonIds.BUTTON_MUTE).setChecked(audioState.isMuted());
+ }
+
+ @Override
+ public void updateButtonStates() {
+ // When the incall screen is ready, this method is called from #setSecondary, even though the
+ // incall button ui is not ready yet. This method is called again once the incall button ui is
+ // ready though, so this operation is safe and will be executed asap.
+ if (inCallButtonGridFragment == null) {
+ return;
+ }
+ int numVisibleButtons =
+ inCallButtonGridFragment.updateButtonStates(
+ buttonControllers, buttonChooser, voiceNetworkType, phoneType);
+
+ int visibility = numVisibleButtons == 0 ? View.GONE : View.VISIBLE;
+ pager.setVisibility(visibility);
+ if (adapter != null && adapter.getCount() > 1) {
+ tabLayout.setVisibility(visibility);
+ }
+ }
+
+ @Override
+ public void updateInCallButtonUiColors() {}
+
+ @Override
+ public Fragment getInCallButtonUiFragment() {
+ return this;
+ }
+
+ @Override
+ public void showAudioRouteSelector() {
+ AudioRouteSelectorDialogFragment.newInstance(inCallButtonUiDelegate.getCurrentAudioState())
+ .show(getChildFragmentManager(), null);
+ }
+
+ @Override
+ public void onAudioRouteSelected(int audioRoute) {
+ inCallButtonUiDelegate.setAudioRoute(audioRoute);
+ }
+
+ @NonNull
+ @Override
+ public ButtonController getButtonController(@InCallButtonIds int id) {
+ for (ButtonController buttonController : buttonControllers) {
+ if (buttonController.getInCallButtonId() == id) {
+ return buttonController;
+ }
+ }
+ Assert.fail();
+ return null;
+ }
+
+ @Override
+ public void onButtonGridCreated(InCallButtonGridFragment inCallButtonGridFragment) {
+ LogUtil.i("InCallFragment.onButtonGridCreated", "InCallUiReady");
+ this.inCallButtonGridFragment = inCallButtonGridFragment;
+ inCallButtonUiDelegate.onInCallButtonUiReady(this);
+ updateButtonStates();
+ }
+
+ @Override
+ public void onButtonGridDestroyed() {
+ LogUtil.i("InCallFragment.onButtonGridCreated", "InCallUiUnready");
+ inCallButtonUiDelegate.onInCallButtonUiUnready();
+ this.inCallButtonGridFragment = null;
+ }
+
+ @Override
+ public boolean isShowingLocationUi() {
+ Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
+ return fragment != null && fragment.isVisible();
+ }
+
+ @Override
+ public void showLocationUi(@Nullable Fragment locationUi) {
+ boolean isShowing = isShowingLocationUi();
+ if (!isShowing && locationUi != null) {
+ // Show the location fragment.
+ getChildFragmentManager()
+ .beginTransaction()
+ .replace(R.id.incall_location_holder, locationUi)
+ .commitAllowingStateLoss();
+ } else if (isShowing && locationUi == null) {
+ // Hide the location fragment
+ Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
+ getChildFragmentManager().beginTransaction().remove(fragment).commitAllowingStateLoss();
+ }
+ }
+}
diff --git a/java/com/android/incallui/incall/impl/InCallPagerAdapter.java b/java/com/android/incallui/incall/impl/InCallPagerAdapter.java
new file mode 100644
index 000000000..50eb4c8c3
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/InCallPagerAdapter.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.incall.impl;
+
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentPagerAdapter;
+import android.text.TextUtils;
+import com.android.dialer.multimedia.MultimediaData;
+import com.android.incallui.sessiondata.MultimediaFragment;
+
+/** View pager adapter for in call ui. */
+public class InCallPagerAdapter extends FragmentPagerAdapter {
+
+ @Nullable private final MultimediaData attachments;
+
+ public InCallPagerAdapter(FragmentManager fragmentManager, MultimediaData attachments) {
+ super(fragmentManager);
+ this.attachments = attachments;
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ if (position == getButtonGridPosition()) {
+ return InCallButtonGridFragment.newInstance();
+ } else {
+ // TODO: handle fragment invalidation for when the data changes.
+ return MultimediaFragment.newInstance(attachments, true, false);
+ }
+ }
+
+ @Override
+ public int getCount() {
+ if (attachments != null
+ && (!TextUtils.isEmpty(attachments.getSubject()) || attachments.hasImageData())) {
+ return 2;
+ }
+ return 1;
+ }
+
+ public int getButtonGridPosition() {
+ return getCount() - 1;
+ }
+}
diff --git a/java/com/android/incallui/incall/impl/MappedButtonConfig.java b/java/com/android/incallui/incall/impl/MappedButtonConfig.java
new file mode 100644
index 000000000..ecdb5dfea
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/MappedButtonConfig.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.incall.impl;
+
+import android.support.annotation.NonNull;
+import android.support.v4.util.ArrayMap;
+import android.util.ArraySet;
+import com.android.dialer.common.Assert;
+import com.android.incallui.incall.protocol.InCallButtonIds;
+import com.android.incallui.incall.protocol.InCallButtonIdsExtension;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Determines logical button slot and ordering based on a provided mapping.
+ *
+ * <p>The provided mapping is declared with the following pieces of information: key, the {@link
+ * InCallButtonIds} for which the mapping applies; {@link MappingInfo#getSlot()}, the arbitrarily
+ * indexed slot into which the InCallButtonId desires to be placed; {@link
+ * MappingInfo#getSlotOrder()}, the slotOrder, used to choose the correct InCallButtonId when
+ * multiple desire to be placed in the same slot; and {@link MappingInfo#getConflictOrder()}, the
+ * conflictOrder, used to determine the overall order for InCallButtonIds that weren't chosen for
+ * their desired slot.
+ */
+@Immutable
+final class MappedButtonConfig {
+
+ @NonNull private final Map<Integer, MappingInfo> mapping;
+ @NonNull private final List<Integer> orderedMappedSlots;
+
+ /**
+ * Creates this MappedButtonConfig with the given mapping of {@link InCallButtonIds} to their
+ * corresponding slots and order.
+ *
+ * @param mapping the mapping.
+ */
+ public MappedButtonConfig(@NonNull Map<Integer, MappingInfo> mapping) {
+ this.mapping = new ArrayMap<>();
+ this.mapping.putAll(Assert.isNotNull(mapping));
+ this.orderedMappedSlots = findOrderedMappedSlots();
+ }
+
+ private List<Integer> findOrderedMappedSlots() {
+ Set<Integer> slots = new ArraySet<>();
+ for (Entry<Integer, MappingInfo> entry : mapping.entrySet()) {
+ slots.add(entry.getValue().getSlot());
+ }
+ List<Integer> orderedSlots = new ArrayList<>(slots);
+ Collections.sort(orderedSlots);
+ return orderedSlots;
+ }
+
+ /** Returns an immutable list of the slots for which this class has button mapping. */
+ @NonNull
+ public List<Integer> getOrderedMappedSlots() {
+ if (mapping.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return Collections.unmodifiableList(orderedMappedSlots);
+ }
+
+ /**
+ * Returns a list of {@link InCallButtonIds} that are configured to be placed in the given ui
+ * slot. The slot can be based from any index, as long as it matches the provided mapping.
+ */
+ @NonNull
+ public List<Integer> getButtonsForSlot(int slot) {
+ List<Integer> buttons = new ArrayList<>();
+ for (Entry<Integer, MappingInfo> entry : mapping.entrySet()) {
+ if (entry.getValue().getSlot() == slot) {
+ buttons.add(entry.getKey());
+ }
+ }
+ return buttons;
+ }
+
+ /**
+ * Returns a {@link Comparator} capable of ordering {@link InCallButtonIds} that are configured to
+ * be placed in the same slot. InCallButtonIds are sorted based on the natural ordering of {@link
+ * MappingInfo#getSlotOrder()}.
+ *
+ * <p>Note: the returned Comparator's compare method will throw an {@link
+ * IllegalArgumentException} if called with InCallButtonIds that have no configuration or are not
+ * to be placed in the same slot.
+ */
+ @NonNull
+ public Comparator<Integer> getSlotComparator() {
+ return new Comparator<Integer>() {
+ @Override
+ public int compare(Integer lhs, Integer rhs) {
+ MappingInfo lhsInfo = lookupMappingInfo(lhs);
+ MappingInfo rhsInfo = lookupMappingInfo(rhs);
+ if (lhsInfo.getSlot() != rhsInfo.getSlot()) {
+ throw new IllegalArgumentException("lhs and rhs don't go in the same slot");
+ }
+ return lhsInfo.getSlotOrder() - rhsInfo.getSlotOrder();
+ }
+ };
+ }
+
+ /**
+ * Returns a {@link Comparator} capable of ordering {@link InCallButtonIds} by their conflict
+ * score. This comparator should be used when multiple InCallButtonIds could have been shown in
+ * the same slot. InCallButtonIds are sorted based on the natural ordering of {@link
+ * MappingInfo#getConflictOrder()}.
+ *
+ * <p>Note: the returned Comparator's compare method will throw an {@link
+ * IllegalArgumentException} if called with InCallButtonIds that have no configuration.
+ */
+ @NonNull
+ public Comparator<Integer> getConflictComparator() {
+ return new Comparator<Integer>() {
+ @Override
+ public int compare(Integer lhs, Integer rhs) {
+ MappingInfo lhsInfo = lookupMappingInfo(lhs);
+ MappingInfo rhsInfo = lookupMappingInfo(rhs);
+ return lhsInfo.getConflictOrder() - rhsInfo.getConflictOrder();
+ }
+ };
+ }
+
+ @NonNull
+ private MappingInfo lookupMappingInfo(@InCallButtonIds int button) {
+ MappingInfo info = mapping.get(button);
+ if (info == null) {
+ throw new IllegalArgumentException(
+ "Unknown InCallButtonId: " + InCallButtonIdsExtension.toString(button));
+ }
+ return info;
+ }
+
+ /** Holds information about button mapping. */
+
+ abstract static class MappingInfo {
+
+ /** The Ui slot into which a given button desires to be placed. */
+ public abstract int getSlot();
+
+ /**
+ * Returns an integer used to determine which button is chosen for a slot when multiple buttons
+ * desire to be placed in the same slot. Follows from the natural ordering of integers, i.e. a
+ * lower slotOrder results in the button being chosen.
+ */
+ public abstract int getSlotOrder();
+
+ /**
+ * Returns an integer used to determine the order in which buttons that weren't chosen for their
+ * desired slot are placed into the Ui. Follows from the natural ordering of integers, i.e. a
+ * lower conflictOrder results in the button being chosen.
+ */
+ public abstract int getConflictOrder();
+
+ static Builder builder(int slot) {
+ return new AutoValue_MappedButtonConfig_MappingInfo.Builder()
+ .setSlot(slot)
+ .setSlotOrder(Integer.MAX_VALUE)
+ .setConflictOrder(Integer.MAX_VALUE);
+ }
+
+ /** Class used to build instances of {@link MappingInfo}. */
+
+ abstract static class Builder {
+ public abstract Builder setSlot(int slot);
+
+ public abstract Builder setSlotOrder(int slotOrder);
+
+ public abstract Builder setConflictOrder(int conflictOrder);
+
+ public abstract MappingInfo build();
+ }
+ }
+}
diff --git a/java/com/android/incallui/incall/impl/res/animator/incall_button_elevation.xml b/java/com/android/incallui/incall/impl/res/animator/incall_button_elevation.xml
new file mode 100644
index 000000000..69215adda
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/animator/incall_button_elevation.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:state_enabled="true"
+ android:state_pressed="true">
+ <objectAnimator
+ android:duration="@android:integer/config_shortAnimTime"
+ android:propertyName="translationZ"
+ android:valueTo="8dp"
+ android:valueType="floatType"/>
+
+ </item>
+ <item
+ android:state_checked="true"
+ android:state_enabled="true"
+ android:state_pressed="false">
+ <objectAnimator
+ android:duration="@android:integer/config_shortAnimTime"
+ android:propertyName="translationZ"
+ android:valueTo="4dp"
+ android:valueType="floatType"/>
+
+ </item>
+ <item>
+ <objectAnimator
+ android:duration="@android:integer/config_shortAnimTime"
+ android:propertyName="translationZ"
+ android:valueTo="0dp"
+ android:valueType="floatType"/>
+ </item>
+</selector>
diff --git a/java/com/android/incallui/incall/impl/res/color/incall_button_icon.xml b/java/com/android/incallui/incall/impl/res/color/incall_button_icon.xml
new file mode 100644
index 000000000..6d8556759
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/color/incall_button_icon.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="#FF01579B" android:state_checked="true"/>
+ <item android:color="#FFFFFFFF"/>
+</selector>
diff --git a/java/com/android/incallui/incall/impl/res/drawable-mdpi/ic_addcall_white.png b/java/com/android/incallui/incall/impl/res/drawable-mdpi/ic_addcall_white.png
new file mode 100644
index 000000000..a60805258
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable-mdpi/ic_addcall_white.png
Binary files differ
diff --git a/java/com/android/incallui/incall/impl/res/drawable-xhdpi/ic_addcall_white.png b/java/com/android/incallui/incall/impl/res/drawable-xhdpi/ic_addcall_white.png
new file mode 100644
index 000000000..d2a843c38
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable-xhdpi/ic_addcall_white.png
Binary files differ
diff --git a/java/com/android/incallui/incall/impl/res/drawable/incall_button_background.xml b/java/com/android/incallui/incall/impl/res/drawable/incall_button_background.xml
new file mode 100644
index 000000000..c8bd29568
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/incall_button_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <selector>
+ <item
+ android:drawable="@drawable/incall_button_background_checked"
+ android:state_checked="true"/>
+ <item android:drawable="@drawable/incall_button_background_unchecked"/>
+ </selector>
+ </item>
+ <item>
+ <ripple android:color="@color/incall_button_ripple">
+ <item
+ android:id="@android:id/mask"
+ android:gravity="center">
+ <shape android:shape="oval">
+ <solid android:color="@android:color/white"/>
+ </shape>
+ </item>
+ </ripple>
+ </item>
+</layer-list>
diff --git a/java/com/android/incallui/incall/impl/res/drawable/incall_button_background_checked.xml b/java/com/android/incallui/incall/impl/res/drawable/incall_button_background_checked.xml
new file mode 100644
index 000000000..73c6947e2
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/incall_button_background_checked.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+ <solid android:color="@color/incall_button_white"/>
+</shape>
diff --git a/java/com/android/incallui/incall/impl/res/drawable/incall_button_background_more.xml b/java/com/android/incallui/incall/impl/res/drawable/incall_button_background_more.xml
new file mode 100644
index 000000000..6755f0fae
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/incall_button_background_more.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <selector>
+ <item
+ android:drawable="@drawable/incall_button_background_checked"
+ android:state_checked="true"/>
+ <item android:drawable="@drawable/incall_button_background_unchecked"/>
+ </selector>
+ </item>
+ <item>
+ <ripple android:color="@color/incall_button_ripple">
+ <item
+ android:id="@android:id/mask"
+ android:gravity="center">
+ <shape android:shape="oval">
+ <solid android:color="@android:color/white"/>
+ </shape>
+ </item>
+ </ripple>
+ </item>
+
+ <!-- This adds a little down arrow to indicate that the button will pop up a menu. Use an explicit
+ <bitmap> to avoid scaling the icon up to the full size of the button. -->
+ <item>
+ <bitmap
+ android:gravity="end"
+ android:src="@drawable/quantum_ic_arrow_drop_down_white_18"/>
+ </item>
+</layer-list>
diff --git a/java/com/android/incallui/incall/impl/res/drawable/incall_button_background_unchecked.xml b/java/com/android/incallui/incall/impl/res/drawable/incall_button_background_unchecked.xml
new file mode 100644
index 000000000..f7ffa4d50
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/incall_button_background_unchecked.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+ <solid android:color="@android:color/transparent"/>
+</shape>
diff --git a/java/com/android/incallui/incall/impl/res/drawable/incall_ic_add_call.xml b/java/com/android/incallui/incall/impl/res/drawable/incall_ic_add_call.xml
new file mode 100644
index 000000000..4daf0527c
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/incall_ic_add_call.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_addcall_white"/>
+</selector>
diff --git a/java/com/android/incallui/incall/impl/res/drawable/incall_ic_dialpad.xml b/java/com/android/incallui/incall/impl/res/drawable/incall_ic_dialpad.xml
new file mode 100644
index 000000000..091142bef
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/incall_ic_dialpad.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/quantum_ic_dialpad_white_36"/>
+</selector>
diff --git a/java/com/android/incallui/incall/impl/res/drawable/incall_ic_manage.xml b/java/com/android/incallui/incall/impl/res/drawable/incall_ic_manage.xml
new file mode 100644
index 000000000..a48e4c4ed
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/incall_ic_manage.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/quantum_ic_group_white_36"/>
+</selector>
diff --git a/java/com/android/incallui/incall/impl/res/drawable/incall_ic_merge.xml b/java/com/android/incallui/incall/impl/res/drawable/incall_ic_merge.xml
new file mode 100644
index 000000000..61d75556e
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/incall_ic_merge.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/quantum_ic_call_merge_white_36"/>
+</selector>
diff --git a/java/com/android/incallui/incall/impl/res/drawable/incall_ic_pause.xml b/java/com/android/incallui/incall/impl/res/drawable/incall_ic_pause.xml
new file mode 100644
index 000000000..6aa8ab8ce
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/incall_ic_pause.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/quantum_ic_pause_white_36"/>
+</selector>
diff --git a/java/com/android/incallui/incall/impl/res/drawable/tab_indicator_default.xml b/java/com/android/incallui/incall/impl/res/drawable/tab_indicator_default.xml
new file mode 100644
index 000000000..6a55b35dc
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/tab_indicator_default.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape
+ android:innerRadius="0dp"
+ android:shape="ring"
+ android:thickness="2dp"
+ android:useLevel="false">
+ <solid android:color="@android:color/darker_gray"/>
+ </shape>
+ </item>
+</layer-list> \ No newline at end of file
diff --git a/java/com/android/incallui/incall/impl/res/drawable/tab_indicator_selected.xml b/java/com/android/incallui/incall/impl/res/drawable/tab_indicator_selected.xml
new file mode 100644
index 000000000..fc673c6ed
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/tab_indicator_selected.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape
+ android:innerRadius="0dp"
+ android:shape="ring"
+ android:thickness="4dp"
+ android:useLevel="false">
+ <solid android:color="@color/background_dialer_white"/>
+ </shape>
+ </item>
+</layer-list> \ No newline at end of file
diff --git a/java/com/android/incallui/incall/impl/res/drawable/tab_selector.xml b/java/com/android/incallui/incall/impl/res/drawable/tab_selector.xml
new file mode 100644
index 000000000..303a49bd8
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/tab_selector.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/tab_indicator_selected"
+ android:state_selected="true"/>
+ <item android:drawable="@drawable/tab_indicator_default"/>
+</selector> \ No newline at end of file
diff --git a/java/com/android/incallui/incall/impl/res/layout/call_composer_data_fragment.xml b/java/com/android/incallui/incall/impl/res/layout/call_composer_data_fragment.xml
new file mode 100644
index 000000000..335ac8ae2
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/layout/call_composer_data_fragment.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <TextView
+ android:id="@+id/subject"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:padding="8dp"
+ android:textSize="24sp"
+ android:textColor="@color/primary_text_color"
+ android:background="@color/background_dialer_white"/>
+</FrameLayout> \ No newline at end of file
diff --git a/java/com/android/incallui/incall/impl/res/layout/frag_incall_voice.xml b/java/com/android/incallui/incall/impl/res/layout/frag_incall_voice.xml
new file mode 100644
index 000000000..9b950462c
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/layout/frag_incall_voice.xml
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
+
+ <LinearLayout
+ android:id="@id/incall_contact_grid"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="12dp"
+ android:layout_marginStart="@dimen/incall_window_margin_horizontal"
+ android:layout_marginEnd="@dimen/incall_window_margin_horizontal"
+ android:gravity="center_horizontal"
+ android:orientation="vertical">
+
+ <ImageView
+ android:id="@id/contactgrid_avatar"
+ android:layout_width="@dimen/incall_avatar_size"
+ android:layout_height="@dimen/incall_avatar_size"
+ android:layout_marginBottom="8dp"
+ android:elevation="2dp"/>
+
+ <include
+ layout="@layout/incall_contactgrid_top_row"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <!-- We have to keep deprecated singleLine to allow long text being truncated with ellipses.
+ b/31396406 -->
+ <com.android.incallui.autoresizetext.AutoResizeTextView
+ android:id="@id/contactgrid_contact_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="4dp"
+ android:singleLine="true"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance.Large"
+ app:autoResizeText_minTextSize="28sp"
+ tools:text="Jake Peralta"
+ tools:ignore="Deprecated"/>
+
+ <include
+ layout="@layout/incall_contactgrid_bottom_row"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <FrameLayout
+ android:id="@+id/incall_location_holder"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+ </LinearLayout>
+
+ <android.support.v4.view.ViewPager
+ android:id="@+id/incall_pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_above="@+id/incall_tab_dots"
+ android:layout_below="@+id/incall_contact_grid"
+ android:layout_centerHorizontal="true"/>
+
+ <android.support.design.widget.TabLayout
+ android:id="@+id/incall_tab_dots"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/incall_end_call"
+ android:visibility="gone"
+ app:tabBackground="@drawable/tab_selector"
+ app:tabGravity="center"
+ app:tabIndicatorHeight="0dp"/>
+
+ <FrameLayout
+ android:id="@+id/incall_dialpad_container"
+ style="@style/DialpadContainer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ tools:background="@android:color/white"
+ tools:visibility="gone"/>
+ <ImageButton
+ android:id="@+id/incall_end_call"
+ style="@style/Incall.Button.End"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="36dp"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"
+ android:contentDescription="@string/incall_content_description_end_call"/>
+ </RelativeLayout>
+
+ <FrameLayout
+ android:id="@id/incall_on_hold_banner"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="top"/>
+</FrameLayout>
diff --git a/java/com/android/incallui/incall/impl/res/layout/incall_button_grid.xml b/java/com/android/incallui/incall/impl/res/layout/incall_button_grid.xml
new file mode 100644
index 000000000..59e99440e
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/layout/incall_button_grid.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/incall_window_margin_horizontal"
+ android:layout_marginEnd="@dimen/incall_window_margin_horizontal"
+ tools:showIn="@layout/frag_incall_voice">
+ <GridLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:columnCount="3"
+ android:orientation="horizontal">
+ <com.android.incallui.incall.impl.CheckableLabeledButton
+ android:id="@+id/incall_first_button"
+ android:layout_width="0dp"
+ android:layout_columnWeight="1"
+ android:enabled="false"
+ android:gravity="center"
+ app:incall_labelText="@string/incall_label_mute"
+ tools:background="#FFFF0000"
+ tools:layout_height="@dimen/tools_button_height"
+ tools:layout_width="@dimen/incall_labeled_button_size"/>
+ <com.android.incallui.incall.impl.CheckableLabeledButton
+ android:id="@+id/incall_second_button"
+ android:layout_width="0dp"
+ android:layout_columnWeight="1"
+ android:enabled="false"
+ android:gravity="center"
+ app:incall_labelText="@string/incall_label_dialpad"
+ tools:background="#FFFF0000"
+ tools:layout_height="@dimen/tools_button_height"
+ tools:layout_width="@dimen/incall_labeled_button_size"/>
+ <com.android.incallui.incall.impl.CheckableLabeledButton
+ android:id="@+id/incall_third_button"
+ android:layout_width="0dp"
+ android:layout_columnWeight="1"
+ android:enabled="false"
+ android:gravity="center"
+ app:incall_labelText="@string/incall_label_speaker"
+ tools:background="#FFFF0000"
+ tools:layout_height="@dimen/tools_button_height"
+ tools:layout_width="@dimen/incall_labeled_button_size"/>
+ <com.android.incallui.incall.impl.CheckableLabeledButton
+ android:id="@+id/incall_fourth_button"
+ android:layout_marginTop="@dimen/incall_button_vertical_padding"
+ android:layout_width="0dp"
+ android:layout_columnWeight="1"
+ android:gravity="center"
+ app:incall_labelText="@string/incall_label_add_call"
+ tools:background="#FFFF0000"
+ tools:layout_height="@dimen/tools_button_height"
+ tools:layout_width="@dimen/incall_labeled_button_size"/>
+ <com.android.incallui.incall.impl.CheckableLabeledButton
+ android:id="@+id/incall_fifth_button"
+ android:layout_width="0dp"
+ android:layout_columnWeight="1"
+ android:layout_marginTop="@dimen/incall_button_vertical_padding"
+ android:gravity="center"
+ app:incall_labelText="@string/incall_label_hold"
+ tools:background="#FFFF0000"
+ tools:layout_height="@dimen/tools_button_height"
+ tools:layout_width="@dimen/incall_labeled_button_size"/>
+ <com.android.incallui.incall.impl.CheckableLabeledButton
+ android:id="@+id/incall_sixth_button"
+ android:layout_width="0dp"
+ android:layout_columnWeight="1"
+ android:layout_marginTop="@dimen/incall_button_vertical_padding"
+ android:gravity="center"
+ app:incall_labelText="@string/incall_label_videocall"
+ tools:background="#FFFF0000"
+ tools:layout_height="@dimen/tools_button_height"
+ tools:layout_width="@dimen/incall_labeled_button_size"/>
+ </GridLayout>
+</FrameLayout>
diff --git a/java/com/android/incallui/incall/impl/res/values-h320dp/dimens.xml b/java/com/android/incallui/incall/impl/res/values-h320dp/dimens.xml
new file mode 100644
index 000000000..1fe0c4db9
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values-h320dp/dimens.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <bool name="incall_dialpad_allowed">true</bool>
+ <integer name="incall_num_rows">1</integer>
+</resources>
diff --git a/java/com/android/incallui/incall/impl/res/values-h385dp/dimens.xml b/java/com/android/incallui/incall/impl/res/values-h385dp/dimens.xml
new file mode 100644
index 000000000..aac42c563
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values-h385dp/dimens.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="incall_avatar_size">64dp</dimen>
+ <dimen name="incall_avatar_marginBottom">8dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/incall/impl/res/values-h480dp/dimens.xml b/java/com/android/incallui/incall/impl/res/values-h480dp/dimens.xml
new file mode 100644
index 000000000..ef1a800ac
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values-h480dp/dimens.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <integer name="incall_num_rows">2</integer>
+</resources>
diff --git a/java/com/android/incallui/incall/impl/res/values-h580dp/dimens.xml b/java/com/android/incallui/incall/impl/res/values-h580dp/dimens.xml
new file mode 100644
index 000000000..1f37cd504
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values-h580dp/dimens.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="incall_avatar_size">88dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/incall/impl/res/values-h580dp/styles.xml b/java/com/android/incallui/incall/impl/res/values-h580dp/styles.xml
new file mode 100644
index 000000000..b58ef4819
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values-h580dp/styles.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+
+ <style name="DialpadContainer">
+ <item name="android:layout_below">@id/incall_contact_grid</item>
+ <item name="android:layout_marginTop">8dp</item>
+ </style>
+</resources>
diff --git a/java/com/android/incallui/incall/impl/res/values-w260dp-h520dp/dimens.xml b/java/com/android/incallui/incall/impl/res/values-w260dp-h520dp/dimens.xml
new file mode 100644
index 000000000..e73eb934c
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values-w260dp-h520dp/dimens.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="incall_button_horizontal_padding">16dp</dimen>
+ <dimen name="incall_button_vertical_padding">16dp</dimen>
+ <dimen name="incall_labeled_button_size">64dp</dimen>
+ <dimen name="tools_button_height">92dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/incall/impl/res/values-w300dp-h540dp/dimens.xml b/java/com/android/incallui/incall/impl/res/values-w300dp-h540dp/dimens.xml
new file mode 100644
index 000000000..502ae72dc
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values-w300dp-h540dp/dimens.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="incall_button_horizontal_padding">32dp</dimen>
+ <dimen name="incall_button_vertical_padding">32dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/incall/impl/res/values/attrs.xml b/java/com/android/incallui/incall/impl/res/values/attrs.xml
new file mode 100644
index 000000000..ed1b2a853
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values/attrs.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <declare-styleable name="CheckableLabeledButton">
+ <attr format="reference" name="incall_icon"/>
+ <attr format="string|reference" name="incall_labelText"/>
+ <attr name="android:enabled"/>
+ </declare-styleable>
+</resources>
diff --git a/java/com/android/incallui/incall/impl/res/values/dimens.xml b/java/com/android/incallui/incall/impl/res/values/dimens.xml
new file mode 100644
index 000000000..249788785
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values/dimens.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="incall_button_label_margin">8dp</dimen>
+ <dimen name="incall_button_elevation">0dp</dimen>
+ <dimen name="incall_end_call_spacing">116dp</dimen>
+ <dimen name="incall_button_padding">4dp</dimen>
+ <dimen name="incall_button_horizontal_padding">8dp</dimen>
+ <dimen name="incall_button_vertical_padding">8dp</dimen>
+ <dimen name="incall_avatar_size">0dp</dimen>
+ <dimen name="incall_avatar_marginBottom">0dp</dimen>
+ <dimen name="incall_labeled_button_size">48dp</dimen>
+ <dimen name="tools_button_height">76dp</dimen>
+ <dimen name="incall_window_margin_horizontal">24dp</dimen>
+
+ <bool name="incall_dialpad_allowed">false</bool>
+ <integer name="incall_num_rows">0</integer>
+</resources>
diff --git a/java/com/android/incallui/incall/impl/res/values/ids.xml b/java/com/android/incallui/incall/impl/res/values/ids.xml
new file mode 100644
index 000000000..e1368f95d
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values/ids.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <item name="incall_on_hold_banner" type="id"/>
+ <item name="incall_button_grid" type="id"/>
+ <item name="incall_contact_grid" type="id"/>
+</resources>
diff --git a/java/com/android/incallui/incall/impl/res/values/strings.xml b/java/com/android/incallui/incall/impl/res/values/strings.xml
new file mode 100644
index 000000000..054ca9687
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Button shown during a phone call to upgrade to video.
+ [CHAR LIMIT=12] -->
+ <string name="incall_label_videocall">Video call</string>
+
+ <!-- Button shown during a phone call to put the call on hold.
+ [CHAR LIMIT=12] -->
+ <string name="incall_label_hold">Hold</string>
+
+ <!-- Button shown during a phone call to add a new phone call.
+ [CHAR LIMIT=12] -->
+ <string name="incall_label_add_call">Add call</string>
+
+ <!-- Button shown during a phone call to mute the microphone.
+ [CHAR LIMIT=12] -->
+ <string name="incall_label_mute">Mute</string>
+
+ <!-- Button shown during a phone call to show the dialpad.
+ [CHAR LIMIT=12] -->
+ <string name="incall_label_dialpad">Keypad</string>
+
+ <!-- Button shown during a phone to route audio from earpiece to speaker phone.
+ [CHAR LIMIT=12] -->
+ <string name="incall_label_speaker">Speaker</string>
+
+ <!-- Talkback text for speaker button status. [CHAR LIMIT=12] -->
+ <string name="incall_talkback_speaker_on">, is on</string>
+
+ <!-- Talkback text for speaker button status. [CHAR LIMIT=12] -->
+ <string name="incall_talkback_speaker_off">, is Off</string>
+
+ <!-- Button shown during a phone to merge two ongoing calls.
+ [CHAR LIMIT=12] -->
+ <string name="incall_label_merge">Merge</string>
+
+ <!-- Button shown during a phone to show the manage conference call screen.
+ [CHAR LIMIT=12] -->
+ <string name="incall_label_manage">Manage</string>
+
+ <string name="a11y_description_incall_label_manage_content">Manage callers</string>
+
+ <!-- Button shown during a phone to swap from the foreground call to the background call.
+ [CHAR LIMIT=12] -->
+ <string name="incall_label_swap">Swap</string>
+
+ <!-- Button shown during a phone to switch the audio route.
+ [CHAR LIMIT=12] -->
+ <string name="incall_label_audio">Sound</string>
+
+ <!-- Used to inform the user that the note associated with an outgoing call has been sent.
+ [CHAR LIMIT=32] -->
+ <string name="incall_note_sent">Note sent</string>
+
+</resources> \ No newline at end of file
diff --git a/java/com/android/incallui/incall/impl/res/values/styles.xml b/java/com/android/incallui/incall/impl/res/values/styles.xml
new file mode 100644
index 000000000..2392574a3
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values/styles.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+
+ <style name="DialpadContainer">
+ <item name="android:layout_alignParentTop">true</item>
+ </style>
+</resources>
diff --git a/java/com/android/incallui/incall/protocol/ContactPhotoType.java b/java/com/android/incallui/incall/protocol/ContactPhotoType.java
new file mode 100644
index 000000000..d79b7550b
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/ContactPhotoType.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.incall.protocol;
+
+import android.support.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Types of contact photos we can have. */
+@Retention(RetentionPolicy.SOURCE)
+@IntDef({
+ ContactPhotoType.DEFAULT_PLACEHOLDER,
+ ContactPhotoType.BUSINESS,
+ ContactPhotoType.CONTACT,
+})
+public @interface ContactPhotoType {
+
+ int DEFAULT_PLACEHOLDER = 0;
+ int BUSINESS = 1;
+ int CONTACT = 2;
+}
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonIds.java b/java/com/android/incallui/incall/protocol/InCallButtonIds.java
new file mode 100644
index 000000000..50ebc6413
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/InCallButtonIds.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.incall.protocol;
+
+import android.support.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Ids for buttons in the in call UI. */
+@Retention(RetentionPolicy.SOURCE)
+@IntDef({
+ InCallButtonIds.BUTTON_AUDIO,
+ InCallButtonIds.BUTTON_MUTE,
+ InCallButtonIds.BUTTON_DIALPAD,
+ InCallButtonIds.BUTTON_HOLD,
+ InCallButtonIds.BUTTON_SWAP,
+ InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO,
+ InCallButtonIds.BUTTON_SWITCH_CAMERA,
+ InCallButtonIds.BUTTON_DOWNGRADE_TO_AUDIO,
+ InCallButtonIds.BUTTON_ADD_CALL,
+ InCallButtonIds.BUTTON_MERGE,
+ InCallButtonIds.BUTTON_PAUSE_VIDEO,
+ InCallButtonIds.BUTTON_MANAGE_VIDEO_CONFERENCE,
+ InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE,
+ InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY,
+ InCallButtonIds.BUTTON_COUNT,
+})
+public @interface InCallButtonIds {
+
+ int BUTTON_AUDIO = 0;
+ int BUTTON_MUTE = 1;
+ int BUTTON_DIALPAD = 2;
+ int BUTTON_HOLD = 3;
+ int BUTTON_SWAP = 4;
+ int BUTTON_UPGRADE_TO_VIDEO = 5;
+ int BUTTON_SWITCH_CAMERA = 6;
+ int BUTTON_DOWNGRADE_TO_AUDIO = 7;
+ int BUTTON_ADD_CALL = 8;
+ int BUTTON_MERGE = 9;
+ int BUTTON_PAUSE_VIDEO = 10;
+ int BUTTON_MANAGE_VIDEO_CONFERENCE = 11;
+ int BUTTON_MANAGE_VOICE_CONFERENCE = 12;
+ int BUTTON_SWITCH_TO_SECONDARY = 13;
+ int BUTTON_COUNT = 14;
+}
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java b/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java
new file mode 100644
index 000000000..6d802e346
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.incall.protocol;
+
+/** Utility class for {@link InCallButtonIds}. */
+public class InCallButtonIdsExtension {
+
+ /**
+ * Converts the given {@link InCallButtonIds} to a human readable string.
+ *
+ * @param id the id to convert.
+ * @return the human readable string.
+ */
+ public static String toString(@InCallButtonIds int id) {
+ if (id == InCallButtonIds.BUTTON_AUDIO) {
+ return "AUDIO";
+ } else if (id == InCallButtonIds.BUTTON_MUTE) {
+ return "MUTE";
+ } else if (id == InCallButtonIds.BUTTON_DIALPAD) {
+ return "DIALPAD";
+ } else if (id == InCallButtonIds.BUTTON_HOLD) {
+ return "HOLD";
+ } else if (id == InCallButtonIds.BUTTON_SWAP) {
+ return "SWAP";
+ } else if (id == InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO) {
+ return "UPGRADE_TO_VIDEO";
+ } else if (id == InCallButtonIds.BUTTON_DOWNGRADE_TO_AUDIO) {
+ return "DOWNGRADE_TO_AUDIO";
+ } else if (id == InCallButtonIds.BUTTON_SWITCH_CAMERA) {
+ return "SWITCH_CAMERA";
+ } else if (id == InCallButtonIds.BUTTON_ADD_CALL) {
+ return "ADD_CALL";
+ } else if (id == InCallButtonIds.BUTTON_MERGE) {
+ return "MERGE";
+ } else if (id == InCallButtonIds.BUTTON_PAUSE_VIDEO) {
+ return "PAUSE_VIDEO";
+ } else if (id == InCallButtonIds.BUTTON_MANAGE_VIDEO_CONFERENCE) {
+ return "MANAGE_VIDEO_CONFERENCE";
+ } else if (id == InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE) {
+ return "MANAGE_VOICE_CONFERENCE";
+ } else if (id == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) {
+ return "SWITCH_TO_SECONDARY";
+ } else {
+ return "INVALID_BUTTON: " + id;
+ }
+ }
+}
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonUi.java b/java/com/android/incallui/incall/protocol/InCallButtonUi.java
new file mode 100644
index 000000000..96d741af3
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/InCallButtonUi.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.incall.protocol;
+
+import android.support.v4.app.Fragment;
+import android.telecom.CallAudioState;
+
+/** Interface for the call button UI. */
+public interface InCallButtonUi {
+
+ void showButton(@InCallButtonIds int buttonId, boolean show);
+
+ void enableButton(@InCallButtonIds int buttonId, boolean enable);
+
+ void setEnabled(boolean on);
+
+ void setHold(boolean on);
+
+ void setCameraSwitched(boolean isBackFacingCamera);
+
+ void setVideoPaused(boolean isPaused);
+
+ void setAudioState(CallAudioState audioState);
+
+ /**
+ * Once showButton() has been called on each of the individual buttons in the UI, call this to
+ * configure the overflow menu appropriately.
+ */
+ void updateButtonStates();
+
+ void updateInCallButtonUiColors();
+
+ Fragment getInCallButtonUiFragment();
+
+ void showAudioRouteSelector();
+}
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java
new file mode 100644
index 000000000..5e69f0e2d
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.incall.protocol;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.telecom.CallAudioState;
+
+/** Callbacks from the module out to the container. */
+public interface InCallButtonUiDelegate {
+
+ void onInCallButtonUiReady(InCallButtonUi inCallButtonUi);
+
+ void onInCallButtonUiUnready();
+
+ void onSaveInstanceState(Bundle outState);
+
+ void onRestoreInstanceState(Bundle savedInstanceState);
+
+ void refreshMuteState();
+
+ void addCallClicked();
+
+ void muteClicked(boolean checked);
+
+ void mergeClicked();
+
+ void holdClicked(boolean checked);
+
+ void swapClicked();
+
+ void showDialpadClicked(boolean checked);
+
+ void changeToVideoClicked();
+
+ void switchCameraClicked(boolean useFrontFacingCamera);
+
+ void toggleCameraClicked();
+
+ void pauseVideoClicked(boolean pause);
+
+ void toggleSpeakerphone();
+
+ CallAudioState getCurrentAudioState();
+
+ void setAudioRoute(int route);
+
+ void onEndCallClicked();
+
+ void showAudioRouteSelector();
+
+ Context getContext();
+}
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonUiDelegateFactory.java b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegateFactory.java
new file mode 100644
index 000000000..ca7d11951
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegateFactory.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.incall.protocol;
+
+/** Callbacks from the module out to the container. */
+public interface InCallButtonUiDelegateFactory {
+
+ InCallButtonUiDelegate newInCallButtonUiDelegate();
+}
diff --git a/java/com/android/incallui/incall/protocol/InCallScreen.java b/java/com/android/incallui/incall/protocol/InCallScreen.java
new file mode 100644
index 000000000..612ad26f5
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/InCallScreen.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.incall.protocol;
+
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import android.view.accessibility.AccessibilityEvent;
+
+/** Interface for the call card module. */
+public interface InCallScreen {
+
+ void setPrimary(@NonNull PrimaryInfo primaryInfo);
+
+ void setSecondary(@NonNull SecondaryInfo secondaryInfo);
+
+ void setCallState(@NonNull PrimaryCallState primaryCallState);
+
+ void setEndCallButtonEnabled(boolean enabled, boolean animate);
+
+ void showManageConferenceCallButton(boolean visible);
+
+ boolean isManageConferenceVisible();
+
+ void dispatchPopulateAccessibilityEvent(AccessibilityEvent event);
+
+ void showNoteSentToast();
+
+ void updateInCallScreenColors();
+
+ void onInCallScreenDialpadVisibilityChange(boolean isShowing);
+
+ int getAnswerAndDialpadContainerResourceId();
+
+ void showLocationUi(Fragment locationUi);
+
+ boolean isShowingLocationUi();
+
+ Fragment getInCallScreenFragment();
+}
diff --git a/java/com/android/incallui/incall/protocol/InCallScreenDelegate.java b/java/com/android/incallui/incall/protocol/InCallScreenDelegate.java
new file mode 100644
index 000000000..b39f9f4a2
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/InCallScreenDelegate.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.incall.protocol;
+
+import android.graphics.drawable.Drawable;
+
+/** Callbacks from the module out to the container. */
+public interface InCallScreenDelegate {
+
+ void onInCallScreenDelegateInit(InCallScreen inCallScreen);
+
+ void onInCallScreenReady();
+
+ void onInCallScreenUnready();
+
+ void onEndCallClicked();
+
+ void onSecondaryInfoClicked();
+
+ void onCallStateButtonClicked();
+
+ void onManageConferenceClicked();
+
+ void onShrinkAnimationComplete();
+
+ void onInCallScreenResumed();
+
+ Drawable getDefaultContactPhotoDrawable();
+}
diff --git a/java/com/android/incallui/incall/protocol/InCallScreenDelegateFactory.java b/java/com/android/incallui/incall/protocol/InCallScreenDelegateFactory.java
new file mode 100644
index 000000000..6706691c8
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/InCallScreenDelegateFactory.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.incall.protocol;
+
+/** Callbacks from the module out to the container. */
+public interface InCallScreenDelegateFactory {
+
+ InCallScreenDelegate newInCallScreenDelegate();
+}
diff --git a/java/com/android/incallui/incall/protocol/PrimaryCallState.java b/java/com/android/incallui/incall/protocol/PrimaryCallState.java
new file mode 100644
index 000000000..782090832
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/PrimaryCallState.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.incall.protocol;
+
+import android.graphics.drawable.Drawable;
+import android.telecom.DisconnectCause;
+import android.telecom.VideoProfile;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import java.util.Locale;
+
+/** State of the primary call. */
+public class PrimaryCallState {
+ public final int state;
+ public final int videoState;
+ @SessionModificationState public final int sessionModificationState;
+ public final DisconnectCause disconnectCause;
+ public final String connectionLabel;
+ public final Drawable connectionIcon;
+ public final String gatewayNumber;
+ public final String callSubject;
+ public final String callbackNumber;
+ public final boolean isWifi;
+ public final boolean isConference;
+ public final boolean isWorkCall;
+ public final boolean isHdAudioCall;
+ public final boolean isForwardedNumber;
+ public final boolean shouldShowContactPhoto;
+ public final long connectTimeMillis;
+ public final boolean isVoiceMailNumber;
+ public final boolean isRemotelyHeld;
+
+ // TODO: Convert to autovalue. b/34502119
+ public static PrimaryCallState createEmptyPrimaryCallState() {
+ return new PrimaryCallState(
+ DialerCall.State.IDLE,
+ VideoProfile.STATE_AUDIO_ONLY,
+ DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST,
+ new DisconnectCause(DisconnectCause.UNKNOWN),
+ null, /* connectionLabel */
+ null, /* connectionIcon */
+ null, /* gatewayNumber */
+ null, /* callSubject */
+ null, /* callbackNumber */
+ false /* isWifi */,
+ false /* isConference */,
+ false /* isWorkCall */,
+ false /* isHdAudioCall */,
+ false /* isForwardedNumber */,
+ false /* shouldShowContactPhoto */,
+ 0,
+ false /* isVoiceMailNumber */,
+ false /* isRemotelyHeld */);
+ }
+
+ public PrimaryCallState(
+ int state,
+ int videoState,
+ @SessionModificationState int sessionModificationState,
+ DisconnectCause disconnectCause,
+ String connectionLabel,
+ Drawable connectionIcon,
+ String gatewayNumber,
+ String callSubject,
+ String callbackNumber,
+ boolean isWifi,
+ boolean isConference,
+ boolean isWorkCall,
+ boolean isHdAudioCall,
+ boolean isForwardedNumber,
+ boolean shouldShowContactPhoto,
+ long connectTimeMillis,
+ boolean isVoiceMailNumber,
+ boolean isRemotelyHeld) {
+ this.state = state;
+ this.videoState = videoState;
+ this.sessionModificationState = sessionModificationState;
+ this.disconnectCause = disconnectCause;
+ this.connectionLabel = connectionLabel;
+ this.connectionIcon = connectionIcon;
+ this.gatewayNumber = gatewayNumber;
+ this.callSubject = callSubject;
+ this.callbackNumber = callbackNumber;
+ this.isWifi = isWifi;
+ this.isConference = isConference;
+ this.isWorkCall = isWorkCall;
+ this.isHdAudioCall = isHdAudioCall;
+ this.isForwardedNumber = isForwardedNumber;
+ this.shouldShowContactPhoto = shouldShowContactPhoto;
+ this.connectTimeMillis = connectTimeMillis;
+ this.isVoiceMailNumber = isVoiceMailNumber;
+ this.isRemotelyHeld = isRemotelyHeld;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US, "PrimaryCallState, state: %d, connectionLabel: %s", state, connectionLabel);
+ }
+}
diff --git a/java/com/android/incallui/incall/protocol/PrimaryInfo.java b/java/com/android/incallui/incall/protocol/PrimaryInfo.java
new file mode 100644
index 000000000..1833ed22e
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/PrimaryInfo.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.incall.protocol;
+
+import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.multimedia.MultimediaData;
+import java.util.Locale;
+
+/** Information about the primary call. */
+public class PrimaryInfo {
+ @Nullable public final String number;
+ @Nullable public final String name;
+ public final boolean nameIsNumber;
+ // This is from contacts and shows the type of number. For example, "Mobile".
+ @Nullable public final String label;
+ @Nullable public final String location;
+ @Nullable public final Drawable photo;
+ @ContactPhotoType public final int photoType;
+ public final boolean isSipCall;
+ public final boolean isContactPhotoShown;
+ public final boolean isWorkCall;
+ public final boolean isSpam;
+ public final boolean answeringDisconnectsOngoingCall;
+ public final boolean shouldShowLocation;
+ // Used for consistent LetterTile coloring.
+ @Nullable public final String contactInfoLookupKey;
+ @Nullable public final MultimediaData multimediaData;
+
+ // TODO: Convert to autovalue. b/34502119
+ public static PrimaryInfo createEmptyPrimaryInfo() {
+ return new PrimaryInfo(
+ null,
+ null,
+ false,
+ null,
+ null,
+ null,
+ ContactPhotoType.DEFAULT_PLACEHOLDER,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ null,
+ null);
+ }
+
+ public PrimaryInfo(
+ @Nullable String number,
+ @Nullable String name,
+ boolean nameIsNumber,
+ @Nullable String location,
+ @Nullable String label,
+ @Nullable Drawable photo,
+ @ContactPhotoType int phototType,
+ boolean isSipCall,
+ boolean isContactPhotoShown,
+ boolean isWorkCall,
+ boolean isSpam,
+ boolean answeringDisconnectsOngoingCall,
+ boolean shouldShowLocation,
+ @Nullable String contactInfoLookupKey,
+ @Nullable MultimediaData multimediaData) {
+ this.number = number;
+ this.name = name;
+ this.nameIsNumber = nameIsNumber;
+ this.location = location;
+ this.label = label;
+ this.photo = photo;
+ this.photoType = phototType;
+ this.isSipCall = isSipCall;
+ this.isContactPhotoShown = isContactPhotoShown;
+ this.isWorkCall = isWorkCall;
+ this.isSpam = isSpam;
+ this.answeringDisconnectsOngoingCall = answeringDisconnectsOngoingCall;
+ this.shouldShowLocation = shouldShowLocation;
+ this.contactInfoLookupKey = contactInfoLookupKey;
+ this.multimediaData = multimediaData;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US,
+ "PrimaryInfo, number: %s, name: %s, location: %s, label: %s, "
+ + "photo: %s, photoType: %d, isPhotoVisible: %b",
+ LogUtil.sanitizePhoneNumber(number),
+ LogUtil.sanitizePii(name),
+ LogUtil.sanitizePii(location),
+ label,
+ photo,
+ photoType,
+ isContactPhotoShown);
+ }
+}
diff --git a/java/com/android/incallui/incall/protocol/SecondaryInfo.java b/java/com/android/incallui/incall/protocol/SecondaryInfo.java
new file mode 100644
index 000000000..cadfca6bf
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/SecondaryInfo.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.incall.protocol;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.android.dialer.common.LogUtil;
+import java.util.Locale;
+
+/** Information about the secondary call. */
+public class SecondaryInfo implements Parcelable {
+ public final boolean shouldShow;
+ public final String name;
+ public final boolean nameIsNumber;
+ public final String label;
+ public final String providerLabel;
+ public final boolean isConference;
+ public final boolean isVideoCall;
+ public final boolean isFullscreen;
+
+ public static SecondaryInfo createEmptySecondaryInfo(boolean isFullScreen) {
+ return new SecondaryInfo(false, null, false, null, null, false, false, isFullScreen);
+ }
+
+ public SecondaryInfo(
+ boolean shouldShow,
+ String name,
+ boolean nameIsNumber,
+ String label,
+ String providerLabel,
+ boolean isConference,
+ boolean isVideoCall,
+ boolean isFullscreen) {
+ this.shouldShow = shouldShow;
+ this.name = name;
+ this.nameIsNumber = nameIsNumber;
+ this.label = label;
+ this.providerLabel = providerLabel;
+ this.isConference = isConference;
+ this.isVideoCall = isVideoCall;
+ this.isFullscreen = isFullscreen;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US,
+ "SecondaryInfo, show: %b, name: %s, label: %s, " + "providerLabel: %s",
+ shouldShow,
+ LogUtil.sanitizePii(name),
+ label,
+ providerLabel);
+ }
+
+ protected SecondaryInfo(Parcel in) {
+ shouldShow = in.readByte() != 0;
+ name = in.readString();
+ nameIsNumber = in.readByte() != 0;
+ label = in.readString();
+ providerLabel = in.readString();
+ isConference = in.readByte() != 0;
+ isVideoCall = in.readByte() != 0;
+ isFullscreen = in.readByte() != 0;
+ }
+
+ public static final Creator<SecondaryInfo> CREATOR =
+ new Creator<SecondaryInfo>() {
+ @Override
+ public SecondaryInfo createFromParcel(Parcel in) {
+ return new SecondaryInfo(in);
+ }
+
+ @Override
+ public SecondaryInfo[] newArray(int size) {
+ return new SecondaryInfo[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeByte((byte) (shouldShow ? 1 : 0));
+ dest.writeString(name);
+ dest.writeByte((byte) (nameIsNumber ? 1 : 0));
+ dest.writeString(label);
+ dest.writeString(providerLabel);
+ dest.writeByte((byte) (isConference ? 1 : 0));
+ dest.writeByte((byte) (isVideoCall ? 1 : 0));
+ dest.writeByte((byte) (isFullscreen ? 1 : 0));
+ }
+}
diff --git a/java/com/android/incallui/latencyreport/LatencyReport.java b/java/com/android/incallui/latencyreport/LatencyReport.java
new file mode 100644
index 000000000..2e1fbd590
--- /dev/null
+++ b/java/com/android/incallui/latencyreport/LatencyReport.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.latencyreport;
+
+import android.os.Bundle;
+import android.os.SystemClock;
+
+/** Tracks latency information for a call. */
+public class LatencyReport {
+
+ public static final long INVALID_TIME = -1;
+ // The following are hidden constants from android.telecom.TelecomManager.
+ private static final String EXTRA_CALL_CREATED_TIME_MILLIS =
+ "android.telecom.extra.CALL_CREATED_TIME_MILLIS";
+ private static final String EXTRA_CALL_TELECOM_ROUTING_START_TIME_MILLIS =
+ "android.telecom.extra.CALL_TELECOM_ROUTING_START_TIME_MILLIS";
+ private static final String EXTRA_CALL_TELECOM_ROUTING_END_TIME_MILLIS =
+ "android.telecom.extra.CALL_TELECOM_ROUTING_END_TIME_MILLIS";
+ private final boolean mWasIncoming;
+
+ // Time elapsed since boot when the call was created by the connection service.
+ private final long mCreatedTimeMillis;
+
+ // Time elapsed since boot when telecom began processing the call.
+ private final long mTelecomRoutingStartTimeMillis;
+
+ // Time elapsed since boot when telecom finished processing the call. This includes things like
+ // looking up contact info and call blocking but before showing any UI.
+ private final long mTelecomRoutingEndTimeMillis;
+
+ // Time elapsed since boot when the call was added to the InCallUi.
+ private final long mCallAddedTimeMillis;
+
+ // Time elapsed since boot when the call was added and call blocking evaluation was completed.
+ private long mCallBlockingTimeMillis = INVALID_TIME;
+
+ // Time elapsed since boot when the call notification was shown.
+ private long mCallNotificationTimeMillis = INVALID_TIME;
+
+ // Time elapsed since boot when the InCallUI was shown.
+ private long mInCallUiShownTimeMillis = INVALID_TIME;
+
+ // Whether the call was shown to the user as a heads up notification instead of a full screen
+ // UI.
+ private boolean mDidDisplayHeadsUpNotification;
+
+ public LatencyReport() {
+ mWasIncoming = false;
+ mCreatedTimeMillis = INVALID_TIME;
+ mTelecomRoutingStartTimeMillis = INVALID_TIME;
+ mTelecomRoutingEndTimeMillis = INVALID_TIME;
+ mCallAddedTimeMillis = SystemClock.elapsedRealtime();
+ }
+
+ public LatencyReport(android.telecom.Call telecomCall) {
+ mWasIncoming = telecomCall.getState() == android.telecom.Call.STATE_RINGING;
+ Bundle extras = telecomCall.getDetails().getIntentExtras();
+ if (extras == null) {
+ mCreatedTimeMillis = INVALID_TIME;
+ mTelecomRoutingStartTimeMillis = INVALID_TIME;
+ mTelecomRoutingEndTimeMillis = INVALID_TIME;
+ } else {
+ mCreatedTimeMillis = extras.getLong(EXTRA_CALL_CREATED_TIME_MILLIS, INVALID_TIME);
+ mTelecomRoutingStartTimeMillis =
+ extras.getLong(EXTRA_CALL_TELECOM_ROUTING_START_TIME_MILLIS, INVALID_TIME);
+ mTelecomRoutingEndTimeMillis =
+ extras.getLong(EXTRA_CALL_TELECOM_ROUTING_END_TIME_MILLIS, INVALID_TIME);
+ }
+ mCallAddedTimeMillis = SystemClock.elapsedRealtime();
+ }
+
+ public boolean getWasIncoming() {
+ return mWasIncoming;
+ }
+
+ public long getCreatedTimeMillis() {
+ return mCreatedTimeMillis;
+ }
+
+ public long getTelecomRoutingStartTimeMillis() {
+ return mTelecomRoutingStartTimeMillis;
+ }
+
+ public long getTelecomRoutingEndTimeMillis() {
+ return mTelecomRoutingEndTimeMillis;
+ }
+
+ public long getCallAddedTimeMillis() {
+ return mCallAddedTimeMillis;
+ }
+
+ public long getCallBlockingTimeMillis() {
+ return mCallBlockingTimeMillis;
+ }
+
+ public void onCallBlockingDone() {
+ if (mCallBlockingTimeMillis == INVALID_TIME) {
+ mCallBlockingTimeMillis = SystemClock.elapsedRealtime();
+ }
+ }
+
+ public long getCallNotificationTimeMillis() {
+ return mCallNotificationTimeMillis;
+ }
+
+ public void onNotificationShown() {
+ if (mCallNotificationTimeMillis == INVALID_TIME) {
+ mCallNotificationTimeMillis = SystemClock.elapsedRealtime();
+ }
+ }
+
+ public long getInCallUiShownTimeMillis() {
+ return mInCallUiShownTimeMillis;
+ }
+
+ public void onInCallUiShown(boolean forFullScreenIntent) {
+ if (mInCallUiShownTimeMillis == INVALID_TIME) {
+ mInCallUiShownTimeMillis = SystemClock.elapsedRealtime();
+ mDidDisplayHeadsUpNotification = mWasIncoming && !forFullScreenIntent;
+ }
+ }
+
+ public boolean getDidDisplayHeadsUpNotification() {
+ return mDidDisplayHeadsUpNotification;
+ }
+}
diff --git a/java/com/android/incallui/legacyblocking/BlockedNumberContentObserver.java b/java/com/android/incallui/legacyblocking/BlockedNumberContentObserver.java
new file mode 100644
index 000000000..9b5335b69
--- /dev/null
+++ b/java/com/android/incallui/legacyblocking/BlockedNumberContentObserver.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.legacyblocking;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.provider.CallLog;
+import android.support.annotation.NonNull;
+import com.android.dialer.common.AsyncTaskExecutor;
+import com.android.dialer.common.AsyncTaskExecutors;
+import com.android.dialer.common.LogUtil;
+import java.util.Objects;
+
+/**
+ * Observes the {@link CallLog} to delete the CallLog entry for a blocked call after it is added.
+ * Automatically de-registers itself {@link #TIMEOUT_MS} ms after registration or if the entry is
+ * found and deleted.
+ */
+public class BlockedNumberContentObserver extends ContentObserver
+ implements DeleteBlockedCallTask.Listener {
+
+ /**
+ * The time after which a {@link BlockedNumberContentObserver} will be automatically unregistered.
+ */
+ public static final int TIMEOUT_MS = 5000;
+
+ @NonNull private final Context context;
+ @NonNull private final Handler handler;
+ private final String number;
+ private final long timeAddedMillis;
+ private final Runnable timeoutRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ unregister();
+ }
+ };
+
+ private final AsyncTaskExecutor asyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor();
+
+ /**
+ * Creates the BlockedNumberContentObserver to delete the new {@link CallLog} entry from the given
+ * blocked number.
+ *
+ * @param number The blocked number.
+ * @param timeAddedMillis The time at which the call from the blocked number was placed.
+ */
+ public BlockedNumberContentObserver(
+ @NonNull Context context, @NonNull Handler handler, String number, long timeAddedMillis) {
+ super(handler);
+ this.context = Objects.requireNonNull(context, "context").getApplicationContext();
+ this.handler = Objects.requireNonNull(handler);
+ this.number = number;
+ this.timeAddedMillis = timeAddedMillis;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ LogUtil.i(
+ "BlockedNumberContentObserver.onChange",
+ "attempting to remove call log entry from blocked number");
+ asyncTaskExecutor.submit(
+ DeleteBlockedCallTask.IDENTIFIER,
+ new DeleteBlockedCallTask(context, this, number, timeAddedMillis));
+ }
+
+ @Override
+ public void onDeleteBlockedCallTaskComplete(boolean didFindEntry) {
+ if (didFindEntry) {
+ unregister();
+ }
+ }
+
+ /**
+ * Registers this {@link ContentObserver} to listen for changes to the {@link CallLog}. If the
+ * CallLog entry is not found before {@link #TIMEOUT_MS}, this ContentObserver automatically
+ * un-registers itself.
+ */
+ public void register() {
+ LogUtil.i("BlockedNumberContentObserver.register", null);
+ context.getContentResolver().registerContentObserver(CallLog.CONTENT_URI, true, this);
+ handler.postDelayed(timeoutRunnable, TIMEOUT_MS);
+ }
+
+ private void unregister() {
+ LogUtil.i("BlockedNumberContentObserver.unregister", null);
+ handler.removeCallbacks(timeoutRunnable);
+ context.getContentResolver().unregisterContentObserver(this);
+ }
+}
diff --git a/java/com/android/incallui/legacyblocking/DeleteBlockedCallTask.java b/java/com/android/incallui/legacyblocking/DeleteBlockedCallTask.java
new file mode 100644
index 000000000..a3f2dfa4d
--- /dev/null
+++ b/java/com/android/incallui/legacyblocking/DeleteBlockedCallTask.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.legacyblocking;
+
+import android.Manifest.permission;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.os.AsyncTask;
+import android.os.Build.VERSION_CODES;
+import android.provider.CallLog;
+import android.support.v4.content.ContextCompat;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.telecom.TelecomUtil;
+import java.util.Objects;
+
+/**
+ * Deletes a blocked call from the call log. This is only used on Android Marshmallow. On later
+ * versions of the OS, call blocking is implemented in the system and there's no need to mess with
+ * the call log.
+ */
+@TargetApi(VERSION_CODES.M)
+public class DeleteBlockedCallTask extends AsyncTask<Void, Void, Long> {
+
+ public static final String IDENTIFIER = "DeleteBlockedCallTask";
+
+ // Try to identify if a call log entry corresponds to a number which was blocked. We match by
+ // by comparing its creation time to the time it was added in the InCallUi and seeing if they
+ // fall within a certain threshold.
+ private static final int MATCH_BLOCKED_CALL_THRESHOLD_MS = 3000;
+
+ private final Context context;
+ private final Listener listener;
+ private final String number;
+ private final long timeAddedMillis;
+
+ /**
+ * Creates the task to delete the new {@link CallLog} entry from the given blocked number.
+ *
+ * @param number The blocked number.
+ * @param timeAddedMillis The time at which the call from the blocked number was placed.
+ */
+ public DeleteBlockedCallTask(
+ Context context, Listener listener, String number, long timeAddedMillis) {
+ this.context = Objects.requireNonNull(context);
+ this.listener = Objects.requireNonNull(listener);
+ this.number = number;
+ this.timeAddedMillis = timeAddedMillis;
+ }
+
+ @Override
+ public Long doInBackground(Void... params) {
+ if (ContextCompat.checkSelfPermission(context, permission.READ_CALL_LOG)
+ != PackageManager.PERMISSION_GRANTED
+ || ContextCompat.checkSelfPermission(context, permission.WRITE_CALL_LOG)
+ != PackageManager.PERMISSION_GRANTED) {
+ LogUtil.i("DeleteBlockedCallTask.doInBackground", "missing call log permissions");
+ return -1L;
+ }
+
+ // First, lookup the call log entry of the most recent call with this number.
+ try (Cursor cursor =
+ context
+ .getContentResolver()
+ .query(
+ TelecomUtil.getCallLogUri(context),
+ CallLogDeleteBlockedCallQuery.PROJECTION,
+ CallLog.Calls.NUMBER + "= ?",
+ new String[] {number},
+ CallLog.Calls.DATE + " DESC LIMIT 1")) {
+
+ // If match is found, delete this call log entry and return the call log entry id.
+ if (cursor != null && cursor.moveToFirst()) {
+ long creationTime = cursor.getLong(CallLogDeleteBlockedCallQuery.DATE_COLUMN_INDEX);
+ if (timeAddedMillis > creationTime
+ && timeAddedMillis - creationTime < MATCH_BLOCKED_CALL_THRESHOLD_MS) {
+ long callLogEntryId = cursor.getLong(CallLogDeleteBlockedCallQuery.ID_COLUMN_INDEX);
+ context
+ .getContentResolver()
+ .delete(
+ TelecomUtil.getCallLogUri(context),
+ CallLog.Calls._ID + " IN (" + callLogEntryId + ")",
+ null);
+ return callLogEntryId;
+ }
+ }
+ }
+ return -1L;
+ }
+
+ @Override
+ public void onPostExecute(Long callLogEntryId) {
+ listener.onDeleteBlockedCallTaskComplete(callLogEntryId >= 0);
+ }
+
+ /** Callback invoked when delete is complete. */
+ public interface Listener {
+
+ void onDeleteBlockedCallTaskComplete(boolean didFindEntry);
+ }
+
+ private static class CallLogDeleteBlockedCallQuery {
+
+ static final String[] PROJECTION = new String[] {CallLog.Calls._ID, CallLog.Calls.DATE};
+
+ static final int ID_COLUMN_INDEX = 0;
+ static final int DATE_COLUMN_INDEX = 1;
+ }
+}
diff --git a/java/com/android/incallui/maps/StaticMapBinding.java b/java/com/android/incallui/maps/StaticMapBinding.java
new file mode 100644
index 000000000..9d24ef27a
--- /dev/null
+++ b/java/com/android/incallui/maps/StaticMapBinding.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.maps;
+
+import android.app.Application;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+
+/** Utility for getting a {@link StaticMapFactory} */
+public class StaticMapBinding {
+
+ @Nullable
+ public static StaticMapFactory get(@NonNull Application application) {
+ if (useTestingInstance) {
+ return testingInstance;
+ }
+ if (application instanceof StaticMapFactory) {
+ return ((StaticMapFactory) application);
+ }
+ return null;
+ }
+
+ private static StaticMapFactory testingInstance;
+ private static boolean useTestingInstance;
+
+ @VisibleForTesting
+ public static void setForTesting(@Nullable StaticMapFactory staticMapFactory) {
+ testingInstance = staticMapFactory;
+ useTestingInstance = true;
+ }
+
+ @VisibleForTesting
+ public static void clearForTesting() {
+ useTestingInstance = false;
+ }
+}
diff --git a/java/com/android/incallui/maps/StaticMapFactory.java b/java/com/android/incallui/maps/StaticMapFactory.java
new file mode 100644
index 000000000..a35013886
--- /dev/null
+++ b/java/com/android/incallui/maps/StaticMapFactory.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.maps;
+
+import android.location.Location;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+
+/** A Factory that can create Fragments for showing a static map */
+public interface StaticMapFactory {
+
+ @NonNull
+ Fragment getStaticMap(@NonNull Location location);
+}
diff --git a/java/com/android/incallui/res/anim/activity_open_enter.xml b/java/com/android/incallui/res/anim/activity_open_enter.xml
new file mode 100644
index 000000000..71cc096b9
--- /dev/null
+++ b/java/com/android/incallui/res/anim/activity_open_enter.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2009, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shareInterpolator="false"
+ android:zAdjustment="top">
+ <alpha
+ android:duration="300"
+ android:fillAfter="true"
+ android:fillBefore="false"
+ android:fillEnabled="true"
+ android:fromAlpha="0.0"
+ android:interpolator="@anim/decelerate_cubic"
+ android:toAlpha="1.0"/>
+ <scale
+ android:duration="300"
+ android:fillAfter="true"
+ android:fillBefore="false"
+ android:fillEnabled="true"
+ android:fromXScale=".8"
+ android:fromYScale=".8"
+ android:interpolator="@anim/decelerate_cubic"
+ android:pivotX="50%p"
+ android:pivotY="50%p"
+ android:toXScale="1.0"
+ android:toYScale="1.0"/>
+</set> \ No newline at end of file
diff --git a/java/com/android/incallui/res/anim/activity_open_exit.xml b/java/com/android/incallui/res/anim/activity_open_exit.xml
new file mode 100644
index 000000000..9b36bb358
--- /dev/null
+++ b/java/com/android/incallui/res/anim/activity_open_exit.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2009, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:background="#ff000000"
+ android:zAdjustment="normal">
+ <alpha
+ android:duration="300"
+ android:fillAfter="true"
+ android:fillBefore="false"
+ android:fillEnabled="true"
+ android:fromAlpha="1.0"
+ android:interpolator="@anim/decelerate_quint"
+ android:toAlpha="0.0"/>
+</set> \ No newline at end of file
diff --git a/java/com/android/incallui/res/anim/decelerate_cubic.xml b/java/com/android/incallui/res/anim/decelerate_cubic.xml
new file mode 100644
index 000000000..c2f41597b
--- /dev/null
+++ b/java/com/android/incallui/res/anim/decelerate_cubic.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2010, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<decelerateInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:factor="1.5"/>
diff --git a/java/com/android/incallui/res/anim/decelerate_quint.xml b/java/com/android/incallui/res/anim/decelerate_quint.xml
new file mode 100644
index 000000000..e55e99c0b
--- /dev/null
+++ b/java/com/android/incallui/res/anim/decelerate_quint.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2010, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<decelerateInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:factor="2.5"/>
diff --git a/java/com/android/incallui/res/anim/on_going_call.xml b/java/com/android/incallui/res/anim/on_going_call.xml
new file mode 100644
index 000000000..3a2e2ba1a
--- /dev/null
+++ b/java/com/android/incallui/res/anim/on_going_call.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
+ android:oneshot="false">
+ <item
+ android:drawable="@drawable/ic_ongoing_phone_24px_01"
+ android:duration="200"/>
+ <item
+ android:drawable="@drawable/ic_ongoing_phone_24px_02"
+ android:duration="200"/>
+ <item
+ android:drawable="@drawable/ic_ongoing_phone_24px_03"
+ android:duration="200"/>
+ <item
+ android:drawable="@drawable/ic_ongoing_phone_24px_04"
+ android:duration="200"/>
+ <item
+ android:drawable="@drawable/ic_ongoing_phone_24px_05"
+ android:duration="200"/>
+ <item
+ android:drawable="@drawable/ic_ongoing_phone_24px_06"
+ android:duration="200"/>
+ <item
+ android:drawable="@drawable/ic_ongoing_phone_24px_07"
+ android:duration="200"/>
+ <item
+ android:drawable="@drawable/ic_ongoing_phone_24px_08"
+ android:duration="200"/>
+ <item
+ android:drawable="@drawable/ic_ongoing_phone_24px_09"
+ android:duration="200"/>
+</animation-list> \ No newline at end of file
diff --git a/java/com/android/incallui/res/color/ota_title_color.xml b/java/com/android/incallui/res/color/ota_title_color.xml
new file mode 100644
index 000000000..bf36f56b9
--- /dev/null
+++ b/java/com/android/incallui/res/color/ota_title_color.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2013 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="#FFA6C839"/>
+</selector>
+
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_block_grey600_24dp.png b/java/com/android/incallui/res/drawable-hdpi/ic_block_grey600_24dp.png
new file mode 100644
index 000000000..1e9294c12
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_block_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_call_end_white_24dp.png b/java/com/android/incallui/res/drawable-hdpi/ic_call_end_white_24dp.png
new file mode 100644
index 000000000..757d339c4
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_call_end_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_call_split_white_24dp.png b/java/com/android/incallui/res/drawable-hdpi/ic_call_split_white_24dp.png
new file mode 100644
index 000000000..4e3dbf55d
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_call_split_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_close_grey600_24dp.png b/java/com/android/incallui/res/drawable-hdpi/ic_close_grey600_24dp.png
new file mode 100644
index 000000000..9ab350e9a
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_close_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_location_on_white_24dp.png b/java/com/android/incallui/res/drawable-hdpi/ic_location_on_white_24dp.png
new file mode 100644
index 000000000..7c281c3f5
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_location_on_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_01.png b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_01.png
new file mode 100644
index 000000000..e4ff6db13
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_01.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_02.png b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_02.png
new file mode 100644
index 000000000..bc2b3d2f8
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_02.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_03.png b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_03.png
new file mode 100644
index 000000000..fa936cbdc
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_03.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_04.png b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_04.png
new file mode 100644
index 000000000..ef5137976
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_04.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_05.png b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_05.png
new file mode 100644
index 000000000..3712d164d
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_05.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_06.png b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_06.png
new file mode 100644
index 000000000..c6a4216a3
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_06.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_07.png b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_07.png
new file mode 100644
index 000000000..e4ff6db13
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_07.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_08.png b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_08.png
new file mode 100644
index 000000000..e4ff6db13
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_08.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_09.png b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_09.png
new file mode 100644
index 000000000..e4ff6db13
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_09.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_person_add_grey600_24dp.png b/java/com/android/incallui/res/drawable-hdpi/ic_person_add_grey600_24dp.png
new file mode 100644
index 000000000..185d03393
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_person_add_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_phone_paused_white_24dp.png b/java/com/android/incallui/res/drawable-hdpi/ic_phone_paused_white_24dp.png
new file mode 100644
index 000000000..a2177f58a
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_phone_paused_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_question_mark.png b/java/com/android/incallui/res/drawable-hdpi/ic_question_mark.png
new file mode 100644
index 000000000..bd9489c85
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_question_mark.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_schedule_white_24dp.png b/java/com/android/incallui/res/drawable-hdpi/ic_schedule_white_24dp.png
new file mode 100644
index 000000000..f3581d104
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_schedule_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/img_business.png b/java/com/android/incallui/res/drawable-hdpi/img_business.png
new file mode 100644
index 000000000..f70634262
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/img_business.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/img_conference.png b/java/com/android/incallui/res/drawable-hdpi/img_conference.png
new file mode 100644
index 000000000..3d9f683a5
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/img_conference.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/img_no_image.png b/java/com/android/incallui/res/drawable-hdpi/img_no_image.png
new file mode 100644
index 000000000..fd0ab3211
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/img_no_image.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/img_phone.png b/java/com/android/incallui/res/drawable-hdpi/img_phone.png
new file mode 100644
index 000000000..748312e6e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/img_phone.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_block_grey600_24dp.png b/java/com/android/incallui/res/drawable-mdpi/ic_block_grey600_24dp.png
new file mode 100644
index 000000000..edd666b73
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_block_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_call_end_white_24dp.png b/java/com/android/incallui/res/drawable-mdpi/ic_call_end_white_24dp.png
new file mode 100644
index 000000000..17eb4824e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_call_end_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_call_split_white_24dp.png b/java/com/android/incallui/res/drawable-mdpi/ic_call_split_white_24dp.png
new file mode 100644
index 000000000..cb7ee1f35
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_call_split_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_close_grey600_24dp.png b/java/com/android/incallui/res/drawable-mdpi/ic_close_grey600_24dp.png
new file mode 100644
index 000000000..73faf52eb
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_close_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_location_on_white_24dp.png b/java/com/android/incallui/res/drawable-mdpi/ic_location_on_white_24dp.png
new file mode 100644
index 000000000..933eb5148
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_location_on_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_01.png b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_01.png
new file mode 100644
index 000000000..ae31e047e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_01.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_02.png b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_02.png
new file mode 100644
index 000000000..67b2b1622
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_02.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_03.png b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_03.png
new file mode 100644
index 000000000..46abea337
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_03.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_04.png b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_04.png
new file mode 100644
index 000000000..0d787ffa4
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_04.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_05.png b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_05.png
new file mode 100644
index 000000000..2da4b40d6
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_05.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_06.png b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_06.png
new file mode 100644
index 000000000..a34cf4d56
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_06.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_07.png b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_07.png
new file mode 100644
index 000000000..ae31e047e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_07.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_08.png b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_08.png
new file mode 100644
index 000000000..ae31e047e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_08.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_09.png b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_09.png
new file mode 100644
index 000000000..ae31e047e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_09.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_person_add_grey600_24dp.png b/java/com/android/incallui/res/drawable-mdpi/ic_person_add_grey600_24dp.png
new file mode 100644
index 000000000..ec3237086
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_person_add_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_phone_paused_white_24dp.png b/java/com/android/incallui/res/drawable-mdpi/ic_phone_paused_white_24dp.png
new file mode 100644
index 000000000..7dc920b2b
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_phone_paused_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_question_mark.png b/java/com/android/incallui/res/drawable-mdpi/ic_question_mark.png
new file mode 100644
index 000000000..594d0b9f7
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_question_mark.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_schedule_white_24dp.png b/java/com/android/incallui/res/drawable-mdpi/ic_schedule_white_24dp.png
new file mode 100644
index 000000000..501ee842e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_schedule_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/img_business.png b/java/com/android/incallui/res/drawable-mdpi/img_business.png
new file mode 100644
index 000000000..90738a7ee
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/img_business.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/img_conference.png b/java/com/android/incallui/res/drawable-mdpi/img_conference.png
new file mode 100644
index 000000000..0694dbd55
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/img_conference.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/img_no_image.png b/java/com/android/incallui/res/drawable-mdpi/img_no_image.png
new file mode 100644
index 000000000..014a1c414
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/img_no_image.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/img_phone.png b/java/com/android/incallui/res/drawable-mdpi/img_phone.png
new file mode 100644
index 000000000..41a1d339d
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/img_phone.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_block_grey600_24dp.png b/java/com/android/incallui/res/drawable-xhdpi/ic_block_grey600_24dp.png
new file mode 100644
index 000000000..36210a8cb
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_block_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_call_end_white_24dp.png b/java/com/android/incallui/res/drawable-xhdpi/ic_call_end_white_24dp.png
new file mode 100644
index 000000000..b00d82edd
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_call_end_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_call_split_white_24dp.png b/java/com/android/incallui/res/drawable-xhdpi/ic_call_split_white_24dp.png
new file mode 100644
index 000000000..218cb1214
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_call_split_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_close_grey600_24dp.png b/java/com/android/incallui/res/drawable-xhdpi/ic_close_grey600_24dp.png
new file mode 100644
index 000000000..a3896c5c6
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_close_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_location_on_white_24dp.png b/java/com/android/incallui/res/drawable-xhdpi/ic_location_on_white_24dp.png
new file mode 100644
index 000000000..814ca8ddc
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_location_on_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_01.png b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_01.png
new file mode 100644
index 000000000..80ad50b59
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_01.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_02.png b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_02.png
new file mode 100644
index 000000000..1fb69a477
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_02.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_03.png b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_03.png
new file mode 100644
index 000000000..2578be1e2
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_03.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_04.png b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_04.png
new file mode 100644
index 000000000..9a5b91fe5
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_04.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_05.png b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_05.png
new file mode 100644
index 000000000..69b472b00
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_05.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_06.png b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_06.png
new file mode 100644
index 000000000..118ea33d0
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_06.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_07.png b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_07.png
new file mode 100644
index 000000000..80ad50b59
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_07.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_08.png b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_08.png
new file mode 100644
index 000000000..80ad50b59
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_08.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_09.png b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_09.png
new file mode 100644
index 000000000..80ad50b59
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_09.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_person_add_grey600_24dp.png b/java/com/android/incallui/res/drawable-xhdpi/ic_person_add_grey600_24dp.png
new file mode 100644
index 000000000..e56481ed7
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_person_add_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_phone_paused_white_24dp.png b/java/com/android/incallui/res/drawable-xhdpi/ic_phone_paused_white_24dp.png
new file mode 100644
index 000000000..a8becf485
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_phone_paused_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_question_mark.png b/java/com/android/incallui/res/drawable-xhdpi/ic_question_mark.png
new file mode 100644
index 000000000..ec915f610
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_question_mark.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_schedule_white_24dp.png b/java/com/android/incallui/res/drawable-xhdpi/ic_schedule_white_24dp.png
new file mode 100644
index 000000000..2e27936a4
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_schedule_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/img_business.png b/java/com/android/incallui/res/drawable-xhdpi/img_business.png
new file mode 100644
index 000000000..7b04d956f
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/img_business.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/img_conference.png b/java/com/android/incallui/res/drawable-xhdpi/img_conference.png
new file mode 100644
index 000000000..b0dbcc2dc
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/img_conference.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/img_no_image.png b/java/com/android/incallui/res/drawable-xhdpi/img_no_image.png
new file mode 100644
index 000000000..4022207d0
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/img_no_image.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/img_phone.png b/java/com/android/incallui/res/drawable-xhdpi/img_phone.png
new file mode 100644
index 000000000..2e0ceec0f
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/img_phone.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_block_grey600_24dp.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_block_grey600_24dp.png
new file mode 100644
index 000000000..9f5120373
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_block_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_call_end_white_24dp.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_call_end_white_24dp.png
new file mode 100644
index 000000000..aeabe4a81
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_call_end_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_call_split_white_24dp.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_call_split_white_24dp.png
new file mode 100644
index 000000000..5ea577716
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_call_split_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_close_grey600_24dp.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_close_grey600_24dp.png
new file mode 100644
index 000000000..22d7aa55e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_close_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_location_on_white_24dp.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_location_on_white_24dp.png
new file mode 100644
index 000000000..078b10d4f
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_location_on_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_01.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_01.png
new file mode 100644
index 000000000..871a1ee75
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_01.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_02.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_02.png
new file mode 100644
index 000000000..028e43b6e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_02.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_03.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_03.png
new file mode 100644
index 000000000..b7dd070e1
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_03.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_04.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_04.png
new file mode 100644
index 000000000..887c803f8
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_04.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_05.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_05.png
new file mode 100644
index 000000000..c6ec16893
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_05.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_06.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_06.png
new file mode 100644
index 000000000..d0b1e8649
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_06.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_07.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_07.png
new file mode 100644
index 000000000..871a1ee75
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_07.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_08.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_08.png
new file mode 100644
index 000000000..871a1ee75
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_08.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_09.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_09.png
new file mode 100644
index 000000000..871a1ee75
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_09.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_person_add_grey600_24dp.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_person_add_grey600_24dp.png
new file mode 100644
index 000000000..c17dfe05f
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_person_add_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_phone_paused_white_24dp.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_phone_paused_white_24dp.png
new file mode 100644
index 000000000..baf0cf27f
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_phone_paused_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_question_mark.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_question_mark.png
new file mode 100644
index 000000000..e3f6d285e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_question_mark.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_schedule_white_24dp.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_schedule_white_24dp.png
new file mode 100644
index 000000000..bfc72736a
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_schedule_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/img_business.png b/java/com/android/incallui/res/drawable-xxhdpi/img_business.png
new file mode 100644
index 000000000..c17e4c9d8
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/img_business.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/img_conference.png b/java/com/android/incallui/res/drawable-xxhdpi/img_conference.png
new file mode 100644
index 000000000..a8dba5ed0
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/img_conference.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/img_no_image.png b/java/com/android/incallui/res/drawable-xxhdpi/img_no_image.png
new file mode 100644
index 000000000..2cf7f23a0
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/img_no_image.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/img_phone.png b/java/com/android/incallui/res/drawable-xxhdpi/img_phone.png
new file mode 100644
index 000000000..4eaaba509
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/img_phone.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/ic_block_grey600_24dp.png b/java/com/android/incallui/res/drawable-xxxhdpi/ic_block_grey600_24dp.png
new file mode 100644
index 000000000..01df2b52b
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/ic_block_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/ic_call_end_white_24dp.png b/java/com/android/incallui/res/drawable-xxxhdpi/ic_call_end_white_24dp.png
new file mode 100644
index 000000000..a6e8a7bc1
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/ic_call_end_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/ic_call_split_white_24dp.png b/java/com/android/incallui/res/drawable-xxxhdpi/ic_call_split_white_24dp.png
new file mode 100644
index 000000000..600cec8e6
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/ic_call_split_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/ic_close_grey600_24dp.png b/java/com/android/incallui/res/drawable-xxxhdpi/ic_close_grey600_24dp.png
new file mode 100644
index 000000000..7d1c061f7
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/ic_close_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/ic_location_on_white_24dp.png b/java/com/android/incallui/res/drawable-xxxhdpi/ic_location_on_white_24dp.png
new file mode 100644
index 000000000..8bcb6f620
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/ic_location_on_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/ic_person_add_grey600_24dp.png b/java/com/android/incallui/res/drawable-xxxhdpi/ic_person_add_grey600_24dp.png
new file mode 100644
index 000000000..e24919737
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/ic_person_add_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/ic_question_mark.png b/java/com/android/incallui/res/drawable-xxxhdpi/ic_question_mark.png
new file mode 100644
index 000000000..1a6bf1eb3
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/ic_question_mark.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/ic_schedule_white_24dp.png b/java/com/android/incallui/res/drawable-xxxhdpi/ic_schedule_white_24dp.png
new file mode 100644
index 000000000..b94f4dfa1
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/ic_schedule_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/img_business.png b/java/com/android/incallui/res/drawable-xxxhdpi/img_business.png
new file mode 100644
index 000000000..88f14e999
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/img_business.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/img_conference.png b/java/com/android/incallui/res/drawable-xxxhdpi/img_conference.png
new file mode 100644
index 000000000..eb42b5552
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/img_conference.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/img_no_image.png b/java/com/android/incallui/res/drawable-xxxhdpi/img_no_image.png
new file mode 100644
index 000000000..216574222
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/img_no_image.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/img_phone.png b/java/com/android/incallui/res/drawable-xxxhdpi/img_phone.png
new file mode 100644
index 000000000..7cbfbd75e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/img_phone.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable/img_conference_automirrored.xml b/java/com/android/incallui/res/drawable/img_conference_automirrored.xml
new file mode 100644
index 000000000..78b2876bc
--- /dev/null
+++ b/java/com/android/incallui/res/drawable/img_conference_automirrored.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:autoMirrored="true"
+ android:src="@drawable/img_conference"/> \ No newline at end of file
diff --git a/java/com/android/incallui/res/drawable/img_no_image_automirrored.xml b/java/com/android/incallui/res/drawable/img_no_image_automirrored.xml
new file mode 100644
index 000000000..9a9ec9706
--- /dev/null
+++ b/java/com/android/incallui/res/drawable/img_no_image_automirrored.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:autoMirrored="true"
+ android:src="@drawable/img_no_image"/> \ No newline at end of file
diff --git a/java/com/android/incallui/res/drawable/incall_background_gradient.xml b/java/com/android/incallui/res/drawable/incall_background_gradient.xml
new file mode 100644
index 000000000..5dd927f0f
--- /dev/null
+++ b/java/com/android/incallui/res/drawable/incall_background_gradient.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <gradient
+ android:angle="270"
+ android:startColor="@color/incall_background_gradient_top"
+ android:centerColor="@color/incall_background_gradient_middle"
+ android:endColor="@color/incall_background_gradient_bottom"/>
+</shape>
diff --git a/java/com/android/incallui/res/drawable/spam_notification_icon.xml b/java/com/android/incallui/res/drawable/spam_notification_icon.xml
new file mode 100644
index 000000000..266897838
--- /dev/null
+++ b/java/com/android/incallui/res/drawable/spam_notification_icon.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item>
+ <shape android:shape="oval">
+ <solid android:color="@color/incall_call_spam_background_color"/>
+ <size
+ android:height="@dimen/notification_large_icon_height"
+ android:width="@dimen/notification_large_icon_width"/>
+ </shape>
+ </item>
+
+ <item
+ android:drawable="@drawable/ic_report_white_36dp"
+ android:gravity="center"/>
+
+</layer-list> \ No newline at end of file
diff --git a/java/com/android/incallui/res/drawable/unknown_notification_icon.xml b/java/com/android/incallui/res/drawable/unknown_notification_icon.xml
new file mode 100644
index 000000000..5ab07eccd
--- /dev/null
+++ b/java/com/android/incallui/res/drawable/unknown_notification_icon.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item>
+ <shape android:shape="oval">
+ <solid android:color="@color/unknown_number_color"/>
+ <size
+ android:height="@dimen/notification_large_icon_height"
+ android:width="@dimen/notification_large_icon_width"/>
+ </shape>
+ </item>
+
+ <item
+ android:drawable="@drawable/ic_question_mark"
+ android:gravity="center"/>
+
+</layer-list> \ No newline at end of file
diff --git a/java/com/android/incallui/res/layout/activity_manage_conference.xml b/java/com/android/incallui/res/layout/activity_manage_conference.xml
new file mode 100644
index 000000000..60512938c
--- /dev/null
+++ b/java/com/android/incallui/res/layout/activity_manage_conference.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/manageConferencePanel"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+</FrameLayout>
diff --git a/java/com/android/incallui/res/layout/caller_in_conference.xml b/java/com/android/incallui/res/layout/caller_in_conference.xml
new file mode 100644
index 000000000..3a6773d20
--- /dev/null
+++ b/java/com/android/incallui/res/layout/caller_in_conference.xml
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="64dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="8dp"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <!-- Caller information -->
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/callerPhoto"
+ android:layout_width="@dimen/contact_browser_list_item_photo_size"
+ android:layout_height="@dimen/contact_browser_list_item_photo_size"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:paddingBottom="2dp"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <!-- Name or number of this caller -->
+ <TextView
+ android:id="@+id/conferenceCallerName"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_marginEnd="2dp"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textColor="@color/conference_call_manager_caller_name_text_color"
+ android:textSize="16sp"/>
+
+ <!-- Number of this caller if name is supplied above -->
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="bottom"
+ android:orientation="horizontal">
+
+ <!-- Number -->
+ <TextView
+ android:id="@+id/conferenceCallerNumber"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dp"
+ android:ellipsize="marquee"
+ android:singleLine="true"
+ android:textColor="@color/conference_call_manager_secondary_text_color"
+ android:textSize="14sp"/>
+
+ <!-- Number type -->
+ <TextView
+ android:id="@+id/conferenceCallerNumberType"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:gravity="start"
+ android:singleLine="true"
+ android:textAllCaps="true"
+ android:textColor="@color/conference_call_manager_secondary_text_color"
+ android:textSize="12sp"/>
+
+ </LinearLayout> <!-- End of caller number -->
+
+ </LinearLayout> <!-- End of caller information -->
+
+ </LinearLayout>
+
+ <!-- "Separate" (i.e. "go private") button for this caller -->
+ <ImageView
+ android:id="@+id/conferenceCallerSeparate"
+ android:layout_width="@dimen/conference_call_manager_button_dimension"
+ android:layout_height="@dimen/conference_call_manager_button_dimension"
+ android:background="?android:selectableItemBackgroundBorderless"
+ android:clickable="true"
+ android:contentDescription="@string/goPrivate"
+ android:scaleType="center"
+ android:src="@drawable/ic_call_split_white_24dp"
+ android:tint="@color/conference_call_manager_icon_color"/>
+
+ <!-- "Disconnect" button which terminates the connection with this caller. -->
+ <ImageButton
+ android:id="@+id/conferenceCallerDisconnect"
+ android:layout_width="@dimen/conference_call_manager_button_dimension"
+ android:layout_height="@dimen/conference_call_manager_button_dimension"
+ android:layout_marginStart="8dp"
+ android:background="?android:selectableItemBackgroundBorderless"
+ android:clickable="true"
+ android:contentDescription="@string/conference_caller_disconnect_content_description"
+ android:scaleType="center"
+ android:src="@drawable/ic_call_end_white_24dp"
+ android:tint="@color/conference_call_manager_icon_color"/>
+
+</LinearLayout> <!-- End of single list element -->
diff --git a/java/com/android/incallui/res/layout/conference_manager_fragment.xml b/java/com/android/incallui/res/layout/conference_manager_fragment.xml
new file mode 100644
index 000000000..c0cc4cdcf
--- /dev/null
+++ b/java/com/android/incallui/res/layout/conference_manager_fragment.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- The "Manage conference" UI. This panel is displayed (instead of
+ the inCallPanel) when the user clicks the "Manage conference"
+ button while on a conference call. -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/manageConferencePanel"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <!-- List of conference participants. -->
+ <ListView
+ android:id="@+id/participantList"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:divider="@null"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:listSelector="@null"/>
+</FrameLayout>
diff --git a/java/com/android/incallui/res/layout/incall_dialpad_fragment.xml b/java/com/android/incallui/res/layout/incall_dialpad_fragment.xml
new file mode 100644
index 000000000..0621d48aa
--- /dev/null
+++ b/java/com/android/incallui/res/layout/incall_dialpad_fragment.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<view xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/dtmf_twelve_key_dialer_view"
+ class="com.android.incallui.DialpadFragment$DialpadSlidingLinearLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <include layout="@layout/dialpad_view"/>
+</view>
diff --git a/java/com/android/incallui/res/layout/incall_screen.xml b/java/com/android/incallui/res/layout/incall_screen.xml
new file mode 100644
index 000000000..9090fb287
--- /dev/null
+++ b/java/com/android/incallui/res/layout/incall_screen.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- In-call Phone UI; see InCallActivity.java. -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <FrameLayout
+ android:id="@+id/main"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <View
+ android:id="@+id/psuedo_black_screen_overlay"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="#000000"
+ android:visibility="gone"
+ android:keepScreenOn="true"/>
+</FrameLayout>
diff --git a/java/com/android/incallui/res/layout/video_call_lte_to_wifi_failed.xml b/java/com/android/incallui/res/layout/video_call_lte_to_wifi_failed.xml
new file mode 100644
index 000000000..bdc4eaff1
--- /dev/null
+++ b/java/com/android/incallui/res/layout/video_call_lte_to_wifi_failed.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="25dp"
+ android:orientation="vertical">
+
+ <CheckBox
+ android:id="@+id/video_call_lte_to_wifi_failed_checkbox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/video_call_lte_to_wifi_failed_do_not_show"
+ android:textSize="@dimen/video_call_lte_to_wifi_failed_do_not_show_text_size"/>
+</LinearLayout>
diff --git a/java/com/android/incallui/res/values-sw360dp/dimens.xml b/java/com/android/incallui/res/values-sw360dp/dimens.xml
new file mode 100644
index 000000000..ad782e809
--- /dev/null
+++ b/java/com/android/incallui/res/values-sw360dp/dimens.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2013 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+
+ <!-- The InCallUI dialpad will sometimes want digits sizes that are different from dialer. -->
+ <dimen name="incall_dialpad_key_number_margin_bottom">
+ @dimen/dialpad_key_number_default_margin_bottom
+ </dimen>
+ <!-- Zero key should have less space between self and text because "+" is smaller -->
+ <dimen name="incall_dialpad_zero_key_number_margin_bottom">
+ @dimen/dialpad_zero_key_number_default_margin_bottom
+ </dimen>
+ <dimen name="incall_dialpad_digits_adjustable_text_size">@dimen/dialpad_digits_text_size</dimen>
+ <dimen name="incall_dialpad_digits_adjustable_height">@dimen/dialpad_digits_height</dimen>
+ <dimen name="incall_dialpad_key_numbers_size">@dimen/dialpad_key_numbers_default_size</dimen>
+
+</resources>
diff --git a/java/com/android/incallui/res/values-w500dp-land/colors.xml b/java/com/android/incallui/res/values-w500dp-land/colors.xml
new file mode 100644
index 000000000..4b0e33ea7
--- /dev/null
+++ b/java/com/android/incallui/res/values-w500dp-land/colors.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <!-- Background color for status bar. For portrait this will be ignored. -->
+ <color name="statusbar_background_color">#000000</color>
+</resources>
diff --git a/java/com/android/incallui/res/values-w500dp-land/dimens.xml b/java/com/android/incallui/res/values-w500dp-land/dimens.xml
new file mode 100644
index 000000000..81090fc80
--- /dev/null
+++ b/java/com/android/incallui/res/values-w500dp-land/dimens.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+
+ <!-- Whether or not the landscape mode layout is currently being used -->
+ <bool name="is_layout_landscape">true</bool>
+
+</resources>
diff --git a/java/com/android/incallui/res/values/animation_constants.xml b/java/com/android/incallui/res/values/animation_constants.xml
new file mode 100644
index 000000000..ac50db21c
--- /dev/null
+++ b/java/com/android/incallui/res/values/animation_constants.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <integer name="reveal_animation_duration">333</integer>
+</resources>
diff --git a/java/com/android/incallui/res/values/colors.xml b/java/com/android/incallui/res/values/colors.xml
new file mode 100644
index 000000000..0c73cdb10
--- /dev/null
+++ b/java/com/android/incallui/res/values/colors.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2013 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+
+ <color name="incall_action_bar_background_color">@color/dialer_theme_color</color>
+ <color name="incall_action_bar_text_color">#ffffff</color>
+
+ <!-- Put on top of each photo, implying 80% darker than usual. -->
+ <color name="on_hold_dim_effect">#cc000000</color>
+
+ <color name="conference_call_manager_caller_name_text_color">#4d4d4d</color>
+ <color name="conference_call_manager_icon_color">#999999</color>
+ <!-- Used with some smaller texts in manage conference screen. -->
+ <color name="conference_call_manager_secondary_text_color">#999999</color>
+
+ <color name="incall_dialpad_background">#ffffff</color>
+ <color name="incall_dialpad_background_pressed">#ccaaaaaa</color>
+ <color name="incall_window_scrim">#b2000000</color>
+
+ <!-- Background color for status bar. For portrait this will be ignored. -->
+ <color name="statusbar_background_color">@color/dialer_theme_color</color>
+
+ <color name="translucent_shadow">#33999999</color>
+
+ <!-- 20% opacity, theme color. -->
+ <color name="incall_dialpad_touch_tint">@color/dialer_theme_color_20pct</color>
+
+ <!-- Background colors for InCallUI. This is a set of colors which pass WCAG
+ AA and all have a contrast ratio over 5:1.
+
+ These colors are also used by InCallUIMaterialColorMapUtils to generate
+ primary activity colors.
+
+ -->
+ <array name="background_colors">
+ <item>#00796B</item>
+ <item>#3367D6</item>
+ <item>#303F9F</item>
+ <item>#7B1FA2</item>
+ <item>#C2185B</item>
+ <item>#C53929</item>
+ <item>#A52714</item>
+ </array>
+
+ <!-- Darker versions of background_colors, two shades darker. These colors are used for the
+ status bar. -->
+ <array name="background_colors_dark">
+ <item>#00695C</item>
+ <item>#2A56C6</item>
+ <item>#283593</item>
+ <item>#6A1B9A</item>
+ <item>#AD1457</item>
+ <item>#B93221</item>
+ <item>#841F10</item>
+ </array>
+
+ <!-- Background color for spam. This color must match one of background_colors above. -->
+ <color name="incall_call_spam_background_color">@color/blocked_contact_background</color>
+
+ <!-- Ripple color used over light backgrounds. -->
+ <color name="ripple_light">#40000000</color>
+
+ <!-- Background color for large notification icon in after call from unknown numbers -->
+ <color name="unknown_number_color">#F4B400</color>
+
+ <color name="incall_background_gradient_top">#E91141BB</color>
+ <color name="incall_background_gradient_middle">#E91141BB</color>
+ <color name="incall_background_gradient_bottom">#CC229FEB</color>
+
+ <color name="incall_background_multiwindow">#E91141BB</color>
+
+ <color name="incall_background_gradient_spam_top">#E5A30B0B</color>
+ <color name="incall_background_gradient_spam_middle">#D6C01111</color>
+ <color name="incall_background_gradient_spam_bottom">#B8E55135</color>
+
+ <color name="incall_background_multiwindow_spam">#E9C22E2E</color>
+</resources>
diff --git a/java/com/android/incallui/res/values/config.xml b/java/com/android/incallui/res/values/config.xml
new file mode 100644
index 000000000..0f3c983b7
--- /dev/null
+++ b/java/com/android/incallui/res/values/config.xml
@@ -0,0 +1,23 @@
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <!-- Determines video calls will automatically enter fullscreen mode after the start of the
+ call. -->
+ <bool name="video_call_auto_fullscreen">true</bool>
+ <!-- The number of milliseconds after which a video call will automatically enter fullscreen
+ mode (requires video_call_auto_fullscreen to be true). -->
+ <integer name="video_call_auto_fullscreen_timeout">5000</integer>
+</resources>
diff --git a/java/com/android/incallui/res/values/dimens.xml b/java/com/android/incallui/res/values/dimens.xml
new file mode 100644
index 000000000..18816f645
--- /dev/null
+++ b/java/com/android/incallui/res/values/dimens.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2013 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <dimen name="incall_action_bar_elevation">3dp</dimen>
+
+ <!-- Margin between the bottom of the "call card" photo
+ and the top of the in-call button cluster. -->
+ <dimen name="in_call_touch_ui_upper_margin">2dp</dimen>
+
+ <!-- Padding at the top and bottom edges of the "provider information" -->
+ <dimen name="provider_info_top_bottom_padding">8dp</dimen>
+
+ <!-- Right padding for name and number fields in the call banner.
+ This padding is used to ensure that ultra-long names or
+ numbers won't overlap the elapsed time indication. -->
+ <dimen name="call_banner_name_number_right_padding">50sp</dimen>
+
+ <!-- The InCallUI dialpad will sometimes want digits sizes that are different
+ from dialer. Note, these are the default sizes for small devices. Larger
+ screen sizes apply the values in values-sw360dp/dimens.xml. -->
+ <dimen name="incall_dialpad_key_number_margin_bottom">1dp</dimen>
+ <!-- Zero key should have less space between self and text because "+" is smaller -->
+ <dimen name="incall_dialpad_zero_key_number_margin_bottom">0dp</dimen>
+ <dimen name="incall_dialpad_digits_adjustable_text_size">20sp</dimen>
+ <dimen name="incall_dialpad_digits_adjustable_height">50dp</dimen>
+ <dimen name="incall_dialpad_key_numbers_size">36sp</dimen>
+
+ <!-- Dimensions for OTA Call Card -->
+ <dimen name="otaactivate_layout_marginTop">10dp</dimen>
+ <dimen name="otalistenprogress_layout_marginTop">5dp</dimen>
+ <dimen name="otasuccessfail_layout_marginTop">10dp</dimen>
+
+ <!-- Dimension used to possibly down-scale high-res photo into what is suitable
+ for notification's large icon. -->
+ <dimen name="notification_icon_size">64dp</dimen>
+
+ <!-- Height of translucent shadow effect -->
+ <dimen name="translucent_shadow_height">2dp</dimen>
+
+ <!-- The smaller dimension of the video preview. When in portrait orientation this is the
+ width of the preview. When in landscape, this is the height. -->
+ <dimen name="video_preview_small_dimension">90dp</dimen>
+
+ <dimen name="conference_call_manager_button_dimension">48dp</dimen>
+
+ <!-- Whether or not the landscape mode layout is currently being used -->
+ <bool name="is_layout_landscape">false</bool>
+
+ <dimen name="video_call_lte_to_wifi_failed_do_not_show_text_size">16sp</dimen>
+
+</resources>
diff --git a/java/com/android/incallui/res/values/strings.xml b/java/com/android/incallui/res/values/strings.xml
new file mode 100644
index 000000000..252d131de
--- /dev/null
+++ b/java/com/android/incallui/res/values/strings.xml
@@ -0,0 +1,367 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2013 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- Official label of the phone app, as seen in "Manage Applications"
+ and other settings UIs. -->
+ <string name="phoneAppLabel" product="default">Phone</string>
+
+ <!-- Official label for the in-call UI. DO NOT TRANSLATE. -->
+ <string name="inCallLabel" translate="false">InCallUI</string>
+
+ <!-- In-call screen: status label for a conference call -->
+ <string name="confCall">Conference call</string>
+ <!-- In-call screen: call lost dialog text -->
+ <string name="call_lost">Call dropped</string>
+
+ <!-- MMI dialog strings -->
+ <!-- Dialog label when an MMI code starts running -->
+
+ <!-- post dial -->
+ <!-- In-call screen: body text of the dialog that appears when we encounter
+ the "wait" character in a phone number to be dialed; this dialog asks the
+ user if it's OK to send the numbers following the "wait". -->
+ <string name="wait_prompt_str">Send the following tones?\n</string>
+ <!-- In-call screen: body text of the dialog that appears when we encounter
+ the "PAUSE" character in a phone number to be dialed; this dialog gives
+ informative message to the user to show the sending numbers following the "Pause". -->
+ <string name="pause_prompt_str">Sending tones\n</string>
+ <!-- In-call screen: button label on the "wait" prompt dialog -->
+ <string name="send_button">Send</string>
+ <!-- In-call screen: button label on the "wait" prompt dialog in CDMA Mode-->
+ <string name="pause_prompt_yes">Yes</string>
+ <!-- In-call screen: button label on the "wait" prompt dialog in CDMA Mode-->
+ <string name="pause_prompt_no">No</string>
+ <!-- In-call screen: on the "wild" character dialog, this is the label
+ for a text widget that lets the user enter the digits that should
+ replace the "wild" character. -->
+ <string name="wild_prompt_str">Replace wild character with</string>
+
+ <!-- In-call screen: status label for a conference call -->
+ <string name="caller_manage_header">Conference call <xliff:g id="conf_call_time">%s</xliff:g></string>
+
+ <!-- Used in FakePhoneActivity test code. DO NOT TRANSLATE. -->
+ <string name="fake_phone_activity_phoneNumber_text" translatable="false">(650) 555-1234</string>
+ <!-- Used in FakePhoneActivity test code. DO NOT TRANSLATE. -->
+ <string name="fake_phone_activity_infoText_text" translatable="false">Incoming phone number</string>
+ <!-- Used in FakePhoneActivity test code. DO NOT TRANSLATE. -->
+ <string name="fake_phone_activity_placeCall_text" translatable="false">Fake Incoming Call</string>
+
+ <!-- Call settings screen, Set voicemail dialog title -->
+ <string name="voicemail_settings_number_label">Voicemail number</string>
+
+ <!-- Notification strings -->
+ <!-- The "label" of the in-call Notification for a dialing call, used
+ as the format string for a Chronometer widget. [CHAR LIMIT=60] -->
+ <string name="notification_dialing">Dialing</string>
+ <!-- Missed call notification message used for a single missed call, including
+ the caller-id info from the missed call -->
+ <string name="notification_missedCallTicker">Missed call from <xliff:g id="missed_call_from">%s</xliff:g></string>
+ <!-- The "label" of the in-call Notification for an ongoing call. [CHAR LIMIT=60] -->
+ <string name="notification_ongoing_call">Ongoing call</string>
+ <!-- The "label" of the in-call Notification for an ongoing work call. [CHAR LIMIT=60] -->
+ <string name="notification_ongoing_work_call">Ongoing work call</string>
+ <!-- The "label" of the in-call Notification for an ongoing call, which is being made over
+ Wi-Fi. [CHAR LIMIT=60] -->
+ <string name="notification_ongoing_call_wifi">Ongoing Wi-Fi call</string>
+ <!-- The "label" of the in-call Notification for an ongoing work call, which is being made
+ over Wi-Fi. [CHAR LIMIT=60] -->
+ <string name="notification_ongoing_work_call_wifi">Ongoing Wi-Fi work call</string>
+ <!-- The "label" of the in-call Notification for a call that's on hold -->
+ <string name="notification_on_hold">On hold</string>
+ <!-- The "label" of the in-call Notification for an incoming ringing call. [CHAR LIMIT=60] -->
+ <string name="notification_incoming_call">Incoming call</string>
+ <!-- The "label" of the in-call Notification for an incoming ringing call. [CHAR LIMIT=60] -->
+ <string name="notification_incoming_work_call">Incoming work call</string>
+ <!-- The "label" of the in-call Notification for an incoming ringing call,
+ which is being made over Wi-Fi. [CHAR LIMIT=60] -->
+ <string name="notification_incoming_call_wifi">Incoming Wi-Fi call</string>
+ <!-- The "label" of the in-call Notification for an incoming ringing work call,
+ which is being made over Wi-Fi. [CHAR LIMIT=60] -->
+ <string name="notification_incoming_work_call_wifi">Incoming Wi-Fi work call</string>
+ <!-- The "label" of the in-call Notification for an incoming ringing spam call. -->
+ <string name="notification_incoming_spam_call">Incoming suspected spam call</string>
+ <!-- The "label" of the in-call Notification for upgrading an existing call to a video call. -->
+ <string name="notification_requesting_video_call">Incoming video request</string>
+ <!-- Label for the "Voicemail" notification item, when expanded. -->
+ <string name="notification_voicemail_title">New voicemail</string>
+ <!-- Label for the expanded "Voicemail" notification item,
+ including a count of messages. -->
+ <string name="notification_voicemail_title_count">New voicemail (<xliff:g id="count">%d</xliff:g>)</string>
+ <!-- Message displayed in the "Voicemail" notification item, allowing the user
+ to dial the indicated number. -->
+ <string name="notification_voicemail_text_format">Dial <xliff:g id="voicemail_number">%s</xliff:g></string>
+ <!-- Message displayed in the "Voicemail" notification item,
+ indicating that there's no voicemail number available -->
+ <string name="notification_voicemail_no_vm_number">Voicemail number unknown</string>
+ <!-- Label for the "No service" notification item, when expanded. -->
+ <string name="notification_network_selection_title">No service</string>
+ <!-- Label for the expanded "No service" notification item, including the
+ operator name set by user -->
+ <string name="notification_network_selection_text">Selected network (<xliff:g id="operator_name">%s</xliff:g>) unavailable</string>
+ <!-- Label for the "Answer call" action. This is the displayed label for the action that answers
+ an incoming call. [CHAR LIMIT=12] -->
+ <string name="notification_action_answer">Answer</string>
+ <!-- Label for "end call" Action.
+ It is displayed in the "Ongoing call" notification, which is shown
+ when the user is outside the in-call screen while the phone call is still
+ active. [CHAR LIMIT=12] -->
+ <string name="notification_action_end_call">Hang up</string>
+ <!-- Label for "Video Call" notification action. This is a displayed on the notification for an
+ incoming video call, and answers the call as a video call. [CHAR LIMIT=12] -->
+ <string name="notification_action_answer_video">Video</string>
+ <!-- Label for "Voice" notification action. This is a displayed on the notification for an
+ incoming video call, and answers the call as an audio call. [CHAR LIMIT=12] -->
+ <string name="notification_action_answer_voice">Voice</string>
+ <!-- Label for "Accept" notification action. This is somewhat generic, and may refer to
+ scenarios such as accepting an incoming call or accepting a video call request.
+ [CHAR LIMIT=12] -->
+ <string name="notification_action_accept">Accept</string>
+ <!-- Label for "Dismiss" notification action. This is somewhat generic, and may refer to
+ scenarios such as declining an incoming call or declining a video call request.
+ [CHAR LIMIT=12] -->
+ <string name="notification_action_dismiss">Decline</string>
+
+ <!-- The "label" of the in-call Notification for an ongoing external call.
+ External calls are a representation of a call which is in progress on the user's other
+ device (e.g. another phone or a watch).
+ [CHAR LIMIT=60] -->
+ <string name="notification_external_call">Ongoing call on another device</string>
+ <!-- The "label" of the in-call Notification for an ongoing external video call.
+ External calls are a representation of a call which is in progress on the user's other
+ device (e.g. another phone or a watch).
+ [CHAR LIMIT=60] -->
+ <string name="notification_external_video_call">Ongoing video call on another device</string>
+ <!-- Notification action displayed for external call notifications. External calls are a
+ representation of a call which is in progress on the user's other device (e.g. another
+ phone or a watch). The "take call" action initiates the process of pulling an external
+ call to the current device.
+ [CHAR LIMIT=30] -->
+ <string name="notification_take_call">Take Call</string>
+ <!-- Notification action displayed for external call notifications. External calls are a
+ representation of a call which is in progress on the user's other device (e.g. another
+ phone or a watch). The "take video call" action initiates the process of pulling an external
+ video call to the current device.
+ [CHAR LIMIT=30] -->
+ <string name="notification_take_video_call">Take Video Call</string>
+ <!-- In-call screen: call failure message displayed in an error dialog -->
+ <string name="incall_error_power_off">To place a call, first turn off Airplane mode.</string>
+ <!-- In-call screen: call failure message displayed in an error dialog.
+ This string is currently unused (see comments in InCallActivity.java.) -->
+ <string name="incall_error_emergency_only">Not registered on network.</string>
+ <!-- In-call screen: call failure message displayed in an error dialog -->
+ <string name="incall_error_out_of_service">Cellular network not available.</string>
+ <!-- In-call screen: call failure message displayed in an error dialog -->
+ <string name="incall_error_no_phone_number_supplied">To place a call, enter a valid number.</string>
+ <!-- In-call screen: call failure message displayed in an error dialog -->
+ <string name="incall_error_call_failed">Can\'t call.</string>
+ <!-- In-call screen: status message displayed in a dialog when starting an MMI -->
+ <string name="incall_status_dialed_mmi">Starting MMI sequence\u2026</string>
+ <!-- In-call screen: message displayed in an error dialog -->
+ <string name="incall_error_supp_service_unknown">Service not supported.</string>
+ <!-- In-call screen: message displayed in an error dialog -->
+ <string name="incall_error_supp_service_switch">Can\'t switch calls.</string>
+ <!-- In-call screen: message displayed in an error dialog -->
+ <string name="incall_error_supp_service_separate">Can\'t separate call.</string>
+ <!-- In-call screen: message displayed in an error dialog -->
+ <string name="incall_error_supp_service_transfer">Can\'t transfer.</string>
+ <!-- In-call screen: message displayed in an error dialog -->
+ <string name="incall_error_supp_service_conference">Can\'t conference.</string>
+ <!-- In-call screen: message displayed in an error dialog -->
+ <string name="incall_error_supp_service_reject">Can\'t reject call.</string>
+ <!-- In-call screen: message displayed in an error dialog -->
+ <string name="incall_error_supp_service_hangup">Can\'t release call(s).</string>
+
+ <!-- Dialog title for the "radio enable" UI for emergency calls -->
+ <string name="emergency_enable_radio_dialog_title">Emergency call</string>
+ <!-- Status message for the "radio enable" UI for emergency calls -->
+ <string name="emergency_enable_radio_dialog_message">Turning on radio\u2026</string>
+ <!-- Status message for the "radio enable" UI for emergency calls -->
+ <string name="emergency_enable_radio_dialog_retry">No service. Trying again\u2026</string>
+
+ <!-- Dialer text on Emergency Dialer -->
+ <!-- Emergency dialer: message displayed in an error dialog -->
+ <string name="dial_emergency_error">Can\'t call. <xliff:g id="non_emergency_number">%s</xliff:g> is not an emergency number.</string>
+ <!-- Emergency dialer: message displayed in an error dialog -->
+ <string name="dial_emergency_empty_error">Can\'t call. Dial an emergency number.</string>
+
+ <!-- Displayed in the text entry box in the dialer when in landscape mode to guide the user
+ to dial using the physical keyboard -->
+ <string name="dialerKeyboardHintText">Use keyboard to dial</string>
+
+ <!-- Message indicating that Video Started flowing for IMS-VT calls -->
+ <string name="player_started">Player Started</string>
+ <!-- Message indicating that Video Stopped flowing for IMS-VT calls -->
+ <string name="player_stopped">Player Stopped</string>
+ <!-- Message indicating that camera failure has occurred for the selected camera and
+ as result camera is not ready -->
+ <string name="camera_not_ready">Camera not ready</string>
+ <!-- Message indicating that camera is ready/available -->
+ <string name="camera_ready">Camera ready</string>
+ <!-- Message indicating unknown call session event -->
+ <string name="unknown_call_session_event">"Unkown call session event"</string>
+
+ <!-- For incoming calls, this is a string we can get from a CDMA network instead of
+ the actual phone number, to indicate there's no number present. DO NOT TRANSLATE. -->
+ <string-array name="absent_num" translatable="false">
+ <item>ABSENT NUMBER</item>
+ <item>ABSENTNUMBER</item>
+ </string-array>
+
+ <!-- Preference for Voicemail service provider under "Voicemail" settings.
+ [CHAR LIMIT=40] -->
+ <string name="voicemail_provider">Service</string>
+
+ <!-- Preference for Voicemail setting of each provider.
+ [CHAR LIMIT=40] -->
+ <string name="voicemail_settings">Setup</string>
+
+ <!-- String to display in voicemail number summary when no voicemail num is set -->
+ <string name="voicemail_number_not_set">&lt;Not set&gt;</string>
+
+ <!-- Title displayed above settings coming after voicemail in the call features screen -->
+ <string name="other_settings">Other call settings</string>
+
+ <!-- Use this to describe the separate conference call button; currently for screen readers through accessibility. -->
+ <string name="goPrivate">go private</string>
+ <!-- Use this to describe the select contact button in EditPhoneNumberPreference; currently for screen readers through accessibility. -->
+ <string name="selectContact">select contact</string>
+
+ <!-- Dialog title for the vibration settings for voicemail notifications [CHAR LIMIT=40] -->
+ <string msgid="8731372580674292759" name="voicemail_notification_vibrate_when_title">Vibrate</string>
+ <!-- Dialog title for the vibration settings for voice mail notifications [CHAR LIMIT=40]-->
+ <string msgid="8995274609647451109" name="voicemail_notification_vibarte_when_dialog_title">Vibrate</string>
+
+ <!-- Voicemail ringtone title. The user clicks on this preference to select
+ which sound to play when a voicemail notification is received.
+ [CHAR LIMIT=30] -->
+ <string name="voicemail_notification_ringtone_title">Sound</string>
+
+ <!-- The default value value for voicemail notification. -->
+ <string name="voicemail_notification_vibrate_when_default" translatable="false">never</string>
+
+ <!-- Actual values used in our code for voicemail notifications. DO NOT TRANSLATE -->
+ <string-array name="voicemail_notification_vibrate_when_values" translatable="false">
+ <item>always</item>
+ <item>silent</item>
+ <item>never</item>
+ </string-array>
+
+ <!-- Title for the category "ringtone", which is shown above ringtone and vibration
+ related settings.
+ [CHAR LIMIT=30] -->
+ <string name="preference_category_ringtone">Ringtone &amp; Vibrate</string>
+
+ <!-- Label for "Manage conference call" panel [CHAR LIMIT=40] -->
+ <string name="manageConferenceLabel">Manage conference call</string>
+
+ <!-- This can be used in any application wanting to disable the text "Emergency number" -->
+ <string name="emergency_call_dialog_number_for_display">Emergency number</string>
+
+ <!-- Used to inform the user that a call was received via a number other than the primary
+ phone number associated with their device. [CHAR LIMIT=16] -->
+ <string name="child_number">via <xliff:g example="650-555-1212" id="child_number">%s</xliff:g></string>
+
+ <!-- Title for the call context with a person-type contact. [CHAR LIMIT=40] -->
+ <string name="person_contact_context_title">Recent messages</string>
+
+ <!-- Title for the call context with a business-type contact. [CHAR LIMIT=40] -->
+ <string name="business_contact_context_title">Business info</string>
+
+ <!-- Distance strings for business caller ID context. -->
+
+ <!-- Used to inform the user how far away a location is in miles. [CHAR LIMIT=NONE] -->
+ <string name="distance_imperial_away"><xliff:g id="distance">%.1f</xliff:g> mi away</string>
+ <!-- Used to inform the user how far away a location is in kilometers. [CHAR LIMIT=NONE] -->
+ <string name="distance_metric_away"><xliff:g id="distance">%.1f</xliff:g> km away</string>
+ <!-- A shortened way to display a business address. Formatted [street address], [city/locality]. -->
+ <string name="display_address"><xliff:g id="street_address">%1$s</xliff:g>, <xliff:g id="locality">%2$s</xliff:g></string>
+ <!-- Used to indicate hours of operation for a location as a time span. e.g. "11 am - 9 pm" [CHAR LIMIT=NONE] -->
+ <string name="open_time_span"><xliff:g id="open_time">%1$s</xliff:g> - <xliff:g id="close_time">%2$s</xliff:g></string>
+ <!-- Used to indicate a series of opening hours for a location.
+ This first argument may be one or more time spans. e.g. "11 am - 9 pm, 9 pm - 11 pm"
+ The second argument is an additional time span. e.g. "11 pm - 1 am"
+ The string is used to build a list of opening hours.
+ [CHAR LIMIT=NONE] -->
+ <string name="opening_hours"><xliff:g id="earlier_times">%1$s</xliff:g>, <xliff:g id="later_time">%2$s</xliff:g></string>
+ <!-- Used to express when a location will open the next day. [CHAR LIMIT=NONE] -->
+ <string name="opens_tomorrow_at">Opens tomorrow at <xliff:g id="open_time">%s</xliff:g></string>
+ <!-- Used to express the next time at which a location will be open today. [CHAR LIMIT=NONE] -->
+ <string name="opens_today_at">Opens today at <xliff:g id="open_time">%s</xliff:g></string>
+ <!-- Used to express the next time at which a location will close today. [CHAR LIMIT=NONE] -->
+ <string name="closes_today_at">Closes at <xliff:g id="close_time">%s</xliff:g></string>
+ <!-- Used to express the next time at which a location closed today if it is already closed. [CHAR LIMIT=NONE] -->
+ <string name="closed_today_at">Closed today at <xliff:g id="close_time">%s</xliff:g></string>
+ <!-- Displayed when a place is open. -->
+ <string name="open_now">Open now</string>
+ <!-- Displayed when a place is closed. -->
+ <string name="closed_now">Closed now</string>
+
+ <!-- Title for the notification to the user after a call from an unknown number ends. [CHAR LIMIT=100] -->
+ <string name="non_spam_notification_title">Know <xliff:g id="number">%1$s</xliff:g>?</string>
+ <!-- Title for the notification to the user after a call from an spammer ends. [CHAR LIMIT=100] -->
+ <string name="spam_notification_title">Is <xliff:g id="number">%1$s</xliff:g> spam?</string>
+ <!-- Text for the toast shown after the user presses block/report spam. [CHAR LIMIT=100] -->
+ <string name="spam_notification_block_report_toast_text"><xliff:g id="number">%1$s</xliff:g> blocked and call was reported as spam.</string>
+ <!-- Text for the toast shown after the user presses not spam. [CHAR LIMIT=100] -->
+ <string name="spam_notification_not_spam_toast_text">Call from <xliff:g id="number">%1$s</xliff:g> reported as not spam.</string>
+ <!-- Text displayed in the collapsed notification to the user after a non-spam call ends. [CHAR LIMIT=100] -->
+ <string name="spam_notification_non_spam_call_collapsed_text">Tap to add to contacts or block spam number.</string>
+ <!-- Text displayed in the expanded notification to the user after a non-spam call ends. [CHAR LIMIT=NONE] -->
+ <string name="spam_notification_non_spam_call_expanded_text">This is the first time this number called you. If this call was spam, you can block this number and report it.</string>
+ <!-- Text displayed in the collapsed notification to the user after a spam call ends. [CHAR LIMIT=100] -->
+ <string name="spam_notification_spam_call_collapsed_text">Tap to report as NOT SPAM, or block it.</string>
+ <!-- Text displayed in the expanded notification to the user after a spam call ends. [CHAR LIMIT=NONE] -->
+ <string name="spam_notification_spam_call_expanded_text">We suspected this to be a spammer. If this call wasn\'t spam, tap "NOT SPAM" to report our mistake.</string>
+ <!-- Text for the reporting spam action in the after call prompt. [CHAR LIMIT=20] -->
+ <string name="spam_notification_report_spam_action_text">Block &amp; report</string>
+ <!-- Text for the adding to contacts action in the after call prompt. [CHAR LIMIT=20] -->
+ <string name="spam_notification_add_contact_action_text">Add contact</string>
+ <!-- Text for the reporting as not spam action in the after call prompt. [CHAR LIMIT=20] -->
+ <string name="spam_notification_not_spam_action_text">Not spam</string>
+ <!-- Text for the blocking spam action in the after call prompt. [CHAR LIMIT=20] -->
+ <string name="spam_notification_block_spam_action_text">Block number</string>
+ <!-- Text for the adding to contacts action in the after call dialog. [CHAR LIMIT=40] -->
+ <string name="spam_notification_dialog_add_contact_action_text">Add to contacts</string>
+ <!-- Text for the blocking and reporting spam action in the after call dialog. [CHAR LIMIT=40] -->
+ <string name="spam_notification_dialog_block_report_spam_action_text">Block &amp; report spam</string>
+ <!-- Text for the marking a call as not spam in the after call dialog. [CHAR LIMIT=40] -->
+ <string name="spam_notification_dialog_was_not_spam_action_text">Not spam</string>
+
+ <string name="callFailed_simError">No SIM or SIM error</string>
+
+ <string name="conference_caller_disconnect_content_description">End call</string>
+
+ <!-- Name for a conference call. Shown in the in call UI and in notifications. -->
+ <string name="conference_call_name">Conference call</string>
+
+ <!-- Name for a generic conference call. Shown in the in call UI. This is used in CDMA where we
+ don't know the precise state of participants in the conference. -->
+ <string name="generic_conference_call_name">In call</string>
+
+ <!-- Displayed when handover from WiFi to Lte occurs during a video call -->
+ <string name="video_call_wifi_to_lte_handover_toast">Continuing call using cellular data…</string>
+
+ <!-- Displayed when WiFi handover from LTE fails during a video call. -->
+ <string name="video_call_lte_to_wifi_failed_title">Couldn\'t switch to Wi-Fi network</string>
+ <string name="video_call_lte_to_wifi_failed_message">Video call will remain on cellular network. Standard
+ data charges may apply.
+ </string>
+ <string name="video_call_lte_to_wifi_failed_do_not_show">Do not show this again</string>
+
+</resources>
diff --git a/java/com/android/incallui/res/values/styles.xml b/java/com/android/incallui/res/values/styles.xml
new file mode 100644
index 000000000..96e3d4d59
--- /dev/null
+++ b/java/com/android/incallui/res/values/styles.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2013 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <drawable name="grayBg">#FF333333</drawable>
+
+ <!-- Theme for the InCallActivity activity. Should have a transparent background for the
+ circular reveal animation for a new outgoing call to work correctly. We don't just use
+ Theme.Black.NoTitleBar directly, since we want any popups or dialogs from the
+ InCallActivity to have the correct Material style. -->
+ <style name="Theme.InCallScreen" parent="@style/Theme.AppCompat.NoActionBar">
+ <item name="android:textColorPrimary">#ffffff</item>
+ <item name="android:textColorSecondary">#DDFFFFFF</item>
+ <item name="android:colorPrimary">@color/dialer_theme_color</item>
+ <item name="android:colorPrimaryDark">@color/dialer_theme_color_dark</item>
+
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ <item name="android:navigationBarColor">@android:color/transparent</item>
+ <item name="android:windowDrawsSystemBarBackgrounds">true</item>
+
+ <item name="dialpad_key_button_touch_tint">@color/incall_dialpad_touch_tint</item>
+ <item name="dialpad_style">@style/InCallDialpad</item>
+ <item name="android:windowAnimationStyle">@null</item>
+ <item name="android:alertDialogTheme">@style/AlertDialogTheme</item>
+
+ <item name="android:windowBackground">@drawable/incall_background_gradient</item>
+ <item name="android:windowShowWallpaper">true</item>
+ </style>
+
+ <style name="Theme.InCallScreen.ManageConference" parent="DialerThemeBase">
+ </style>
+
+ <style name="InCallDialpad" parent="Dialpad.Light">
+ <item name="dialpad_key_number_margin_bottom">
+ @dimen/incall_dialpad_key_number_margin_bottom
+ </item>
+ <item name="dialpad_zero_key_number_margin_bottom">
+ @dimen/incall_dialpad_zero_key_number_margin_bottom
+ </item>
+ <item name="dialpad_digits_adjustable_text_size">
+ @dimen/incall_dialpad_digits_adjustable_text_size
+ </item>
+ <item name="dialpad_digits_adjustable_height">
+ @dimen/incall_dialpad_digits_adjustable_height
+ </item>
+ <item name="dialpad_key_numbers_size">
+ @dimen/incall_dialpad_key_numbers_size
+ </item>
+ <item name="dialpad_end_key_spacing">
+ @dimen/incall_end_call_spacing
+ </item>
+ </style>
+
+ <style name="AfterCallNotificationTheme" parent="@style/Theme.AppCompat.Light.Dialog.MinWidth">
+ <!-- This colorAccent is to style checkboxes in the dialogs -->
+ <item name="colorAccent">@color/dialer_theme_color</item>
+ <!-- This is needed to make any alert dialogs in this activity take up minimum space -->
+ <item name="android:alertDialogTheme">@style/AfterCallDialogStyle</item>
+ </style>
+
+ <style name="AfterCallDialogStyle" parent="@style/Theme.AppCompat.Light.Dialog.MinWidth">
+ <!-- This colorAccent is to style text in the dialogs -->
+ <item name="android:colorAccent">@color/dialer_theme_color</item>
+ </style>
+
+</resources>
diff --git a/java/com/android/incallui/ringtone/DialerRingtoneManager.java b/java/com/android/incallui/ringtone/DialerRingtoneManager.java
new file mode 100644
index 000000000..5ebd93378
--- /dev/null
+++ b/java/com/android/incallui/ringtone/DialerRingtoneManager.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.ringtone;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.Settings;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall.State;
+import java.util.Objects;
+
+/**
+ * Class that determines when ringtones should be played and can play the call waiting tone when
+ * necessary.
+ */
+public class DialerRingtoneManager {
+
+ /*
+ * Flag used to determine if the Dialer is responsible for playing ringtones for incoming calls.
+ * Once we're ready to enable Dialer Ringing, these flags should be removed.
+ */
+ private static final boolean IS_DIALER_RINGING_ENABLED = false;
+ private final InCallTonePlayer mInCallTonePlayer;
+ private final CallList mCallList;
+ private Boolean mIsDialerRingingEnabledForTesting;
+
+ /**
+ * Creates the DialerRingtoneManager with the given {@link InCallTonePlayer}.
+ *
+ * @param inCallTonePlayer the tone player used to play in-call tones.
+ * @param callList the CallList used to check for {@link State#CALL_WAITING}
+ * @throws NullPointerException if inCallTonePlayer or callList are null
+ */
+ public DialerRingtoneManager(
+ @NonNull InCallTonePlayer inCallTonePlayer, @NonNull CallList callList) {
+ mInCallTonePlayer = Objects.requireNonNull(inCallTonePlayer);
+ mCallList = Objects.requireNonNull(callList);
+ }
+
+ /**
+ * Determines if a ringtone should be played for the given call state (see {@link State}) and
+ * {@link Uri}.
+ *
+ * @param callState the call state for the call being checked.
+ * @param ringtoneUri the ringtone to potentially play.
+ * @return {@code true} if the ringtone should be played, {@code false} otherwise.
+ */
+ public boolean shouldPlayRingtone(int callState, @Nullable Uri ringtoneUri) {
+ return isDialerRingingEnabled()
+ && translateCallStateForCallWaiting(callState) == State.INCOMING
+ && ringtoneUri != null;
+ }
+
+ /**
+ * Determines if an incoming call should vibrate as well as ring.
+ *
+ * @param resolver {@link ContentResolver} used to look up the {@link
+ * Settings.System#VIBRATE_WHEN_RINGING} setting.
+ * @return {@code true} if the call should vibrate, {@code false} otherwise.
+ */
+ public boolean shouldVibrate(ContentResolver resolver) {
+ return Settings.System.getInt(resolver, Settings.System.VIBRATE_WHEN_RINGING, 0) != 0;
+ }
+
+ /**
+ * The incoming callState is never set as {@link State#CALL_WAITING} because {@link
+ * DialerCall#translateState(int)} doesn't account for that case, check for it here
+ */
+ private int translateCallStateForCallWaiting(int callState) {
+ if (callState != State.INCOMING) {
+ return callState;
+ }
+ return mCallList.getActiveCall() == null ? State.INCOMING : State.CALL_WAITING;
+ }
+
+ private boolean isDialerRingingEnabled() {
+ boolean enabledFlag =
+ mIsDialerRingingEnabledForTesting != null
+ ? mIsDialerRingingEnabledForTesting
+ : IS_DIALER_RINGING_ENABLED;
+ return VERSION.SDK_INT >= VERSION_CODES.N && enabledFlag;
+ }
+
+ /**
+ * Determines if a call waiting tone should be played for the the given call state (see {@link
+ * State}).
+ *
+ * @param callState the call state for the call being checked.
+ * @return {@code true} if the call waiting tone should be played, {@code false} otherwise.
+ */
+ public boolean shouldPlayCallWaitingTone(int callState) {
+ return isDialerRingingEnabled()
+ && translateCallStateForCallWaiting(callState) == State.CALL_WAITING
+ && !mInCallTonePlayer.isPlayingTone();
+ }
+
+ /** Plays the call waiting tone. */
+ public void playCallWaitingTone() {
+ if (!isDialerRingingEnabled()) {
+ return;
+ }
+ mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING);
+ }
+
+ /** Stops playing the call waiting tone. */
+ public void stopCallWaitingTone() {
+ if (!isDialerRingingEnabled()) {
+ return;
+ }
+ mInCallTonePlayer.stop();
+ }
+
+ void setDialerRingingEnabledForTesting(boolean status) {
+ mIsDialerRingingEnabledForTesting = status;
+ }
+}
diff --git a/java/com/android/incallui/ringtone/InCallTonePlayer.java b/java/com/android/incallui/ringtone/InCallTonePlayer.java
new file mode 100644
index 000000000..c76b41d72
--- /dev/null
+++ b/java/com/android/incallui/ringtone/InCallTonePlayer.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.ringtone;
+
+import android.media.AudioManager;
+import android.media.ToneGenerator;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.incallui.Log;
+import com.android.incallui.async.PausableExecutor;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Class responsible for playing in-call related tones in a background thread. This class only
+ * allows one tone to be played at a time.
+ */
+public class InCallTonePlayer {
+
+ public static final int TONE_CALL_WAITING = 4;
+
+ public static final int VOLUME_RELATIVE_HIGH_PRIORITY = 80;
+
+ @NonNull private final ToneGeneratorFactory mToneGeneratorFactory;
+ @NonNull private final PausableExecutor mExecutor;
+ private @Nullable CountDownLatch mNumPlayingTones;
+
+ /**
+ * Creates a new InCallTonePlayer.
+ *
+ * @param toneGeneratorFactory the {@link ToneGeneratorFactory} used to create {@link
+ * ToneGenerator}s.
+ * @param executor the {@link PausableExecutor} used to play tones in a background thread.
+ * @throws NullPointerException if audioModeProvider, toneGeneratorFactory, or executor are {@code
+ * null}.
+ */
+ public InCallTonePlayer(
+ @NonNull ToneGeneratorFactory toneGeneratorFactory, @NonNull PausableExecutor executor) {
+ mToneGeneratorFactory = Objects.requireNonNull(toneGeneratorFactory);
+ mExecutor = Objects.requireNonNull(executor);
+ }
+
+ /** @return {@code true} if a tone is currently playing, {@code false} otherwise. */
+ public boolean isPlayingTone() {
+ return mNumPlayingTones != null && mNumPlayingTones.getCount() > 0;
+ }
+
+ /**
+ * Plays the given tone in a background thread.
+ *
+ * @param tone the tone to play.
+ * @throws IllegalStateException if a tone is already playing.
+ * @throws IllegalArgumentException if the tone is invalid.
+ */
+ public void play(int tone) {
+ if (isPlayingTone()) {
+ throw new IllegalStateException("Tone already playing");
+ }
+ final ToneGeneratorInfo info = getToneGeneratorInfo(tone);
+ mNumPlayingTones = new CountDownLatch(1);
+ mExecutor.execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ playOnBackgroundThread(info);
+ }
+ });
+ }
+
+ private ToneGeneratorInfo getToneGeneratorInfo(int tone) {
+ switch (tone) {
+ case TONE_CALL_WAITING:
+ /*
+ * DialerCall waiting tones play until they're stopped either by the user accepting or
+ * declining the call so the tone length is set at what's effectively forever. The
+ * tone is played at a high priority volume and through STREAM_VOICE_CALL since it's
+ * call related and using that stream will route it through bluetooth devices
+ * appropriately.
+ */
+ return new ToneGeneratorInfo(
+ ToneGenerator.TONE_SUP_CALL_WAITING,
+ VOLUME_RELATIVE_HIGH_PRIORITY,
+ Integer.MAX_VALUE,
+ AudioManager.STREAM_VOICE_CALL);
+ default:
+ throw new IllegalArgumentException("Bad tone: " + tone);
+ }
+ }
+
+ private void playOnBackgroundThread(ToneGeneratorInfo info) {
+ ToneGenerator toneGenerator = null;
+ try {
+ Log.v(this, "Starting tone " + info);
+ toneGenerator = mToneGeneratorFactory.newInCallToneGenerator(info.stream, info.volume);
+ toneGenerator.startTone(info.tone);
+ /*
+ * During tests, this will block until the tests call mExecutor.ackMilestone. This call
+ * allows for synchronization to the point where the tone has started playing.
+ */
+ mExecutor.milestone();
+ if (mNumPlayingTones != null) {
+ mNumPlayingTones.await(info.toneLengthMillis, TimeUnit.MILLISECONDS);
+ // Allows for synchronization to the point where the tone has completed playing.
+ mExecutor.milestone();
+ }
+ } catch (InterruptedException e) {
+ Log.w(this, "Interrupted while playing in-call tone.");
+ } finally {
+ if (toneGenerator != null) {
+ toneGenerator.release();
+ }
+ if (mNumPlayingTones != null) {
+ mNumPlayingTones.countDown();
+ }
+ // Allows for synchronization to the point where this background thread has cleaned up.
+ mExecutor.milestone();
+ }
+ }
+
+ /** Stops playback of the current tone. */
+ public void stop() {
+ if (mNumPlayingTones != null) {
+ mNumPlayingTones.countDown();
+ }
+ }
+
+ private static class ToneGeneratorInfo {
+
+ public final int tone;
+ public final int volume;
+ public final int toneLengthMillis;
+ public final int stream;
+
+ public ToneGeneratorInfo(int toneGeneratorType, int volume, int toneLengthMillis, int stream) {
+ this.tone = toneGeneratorType;
+ this.volume = volume;
+ this.toneLengthMillis = toneLengthMillis;
+ this.stream = stream;
+ }
+
+ @Override
+ public String toString() {
+ return "ToneGeneratorInfo{"
+ + "toneLengthMillis="
+ + toneLengthMillis
+ + ", tone="
+ + tone
+ + ", volume="
+ + volume
+ + '}';
+ }
+ }
+}
diff --git a/java/com/android/incallui/ringtone/ToneGeneratorFactory.java b/java/com/android/incallui/ringtone/ToneGeneratorFactory.java
new file mode 100644
index 000000000..cd7b11aa9
--- /dev/null
+++ b/java/com/android/incallui/ringtone/ToneGeneratorFactory.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.ringtone;
+
+import android.media.ToneGenerator;
+
+/** Factory used to create {@link ToneGenerator}s. */
+public class ToneGeneratorFactory {
+
+ /**
+ * Creates a new {@link ToneGenerator} to use while in a call.
+ *
+ * @param stream the stream through which to play tones.
+ * @param volume the volume at which to play tones.
+ * @return a new ToneGenerator.
+ */
+ public ToneGenerator newInCallToneGenerator(int stream, int volume) {
+ return new ToneGenerator(stream, volume);
+ }
+}
diff --git a/java/com/android/incallui/sessiondata/AndroidManifest.xml b/java/com/android/incallui/sessiondata/AndroidManifest.xml
new file mode 100644
index 000000000..11babd94d
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<manifest
+ package="com.android.incallui.sessiondata">
+</manifest>
diff --git a/java/com/android/incallui/sessiondata/AvatarPresenter.java b/java/com/android/incallui/sessiondata/AvatarPresenter.java
new file mode 100644
index 000000000..e7303b90a
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/AvatarPresenter.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.sessiondata;
+
+import android.support.annotation.Nullable;
+import android.widget.ImageView;
+
+/** Interface for interacting with Fragments that can be put in the data container */
+public interface AvatarPresenter {
+
+ @Nullable
+ ImageView getAvatarImageView();
+
+ int getAvatarSize();
+
+ boolean shouldShowAnonymousAvatar();
+}
diff --git a/java/com/android/incallui/sessiondata/MultimediaFragment.java b/java/com/android/incallui/sessiondata/MultimediaFragment.java
new file mode 100644
index 000000000..d6f671d58
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/MultimediaFragment.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.sessiondata;
+
+import android.graphics.drawable.Drawable;
+import android.location.Location;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.app.Fragment;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.FragmentUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.multimedia.MultimediaData;
+import com.android.incallui.maps.StaticMapBinding;
+import com.android.incallui.maps.StaticMapFactory;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.DataSource;
+import com.bumptech.glide.load.engine.GlideException;
+import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
+import com.bumptech.glide.request.RequestListener;
+import com.bumptech.glide.request.target.Target;
+
+/**
+ * Displays info from {@link MultimediaData MultimediaData}.
+ *
+ * <p>Currently displays image, location (as a map), and message that come bundled with
+ * MultimediaData when calling {@link #newInstance(MultimediaData, boolean, boolean)}.
+ */
+public class MultimediaFragment extends Fragment implements AvatarPresenter {
+
+ private static final String ARG_SUBJECT = "subject";
+ private static final String ARG_IMAGE = "image";
+ private static final String ARG_LOCATION = "location";
+ private static final String ARG_INTERACTIVE = "interactive";
+ private static final String ARG_SHOW_AVATAR = "show_avatar";
+ private ImageView avatarImageView;
+ // TODO: add click listeners
+ @SuppressWarnings("unused")
+ private boolean isInteractive;
+
+ private boolean showAvatar;
+ private StaticMapFactory mapFactory;
+
+ public static MultimediaFragment newInstance(
+ @NonNull MultimediaData multimediaData, boolean isInteractive, boolean showAvatar) {
+ return newInstance(
+ multimediaData.getSubject(),
+ multimediaData.getImageUri(),
+ multimediaData.getLocation(),
+ isInteractive,
+ showAvatar);
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ public static MultimediaFragment newInstance(
+ @Nullable String subject,
+ @Nullable Uri imageUri,
+ @Nullable Location location,
+ boolean isInteractive,
+ boolean showAvatar) {
+ Bundle args = new Bundle();
+ args.putString(ARG_SUBJECT, subject);
+ args.putParcelable(ARG_IMAGE, imageUri);
+ args.putParcelable(ARG_LOCATION, location);
+ args.putBoolean(ARG_INTERACTIVE, isInteractive);
+ args.putBoolean(ARG_SHOW_AVATAR, showAvatar);
+ MultimediaFragment fragment = new MultimediaFragment();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle bundle) {
+ super.onCreate(bundle);
+ isInteractive = getArguments().getBoolean(ARG_INTERACTIVE);
+ showAvatar = getArguments().getBoolean(ARG_SHOW_AVATAR);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+ boolean hasImage = getImageUri() != null;
+ boolean hasSubject = !TextUtils.isEmpty(getSubject());
+ boolean hasMap = getLocation() != null;
+ if (hasMap) {
+ mapFactory = StaticMapBinding.get(getActivity().getApplication());
+ }
+ if (mapFactory != null) {
+ if (hasImage) {
+ if (hasSubject) {
+ return layoutInflater.inflate(
+ R.layout.fragment_composer_text_image_frag, viewGroup, false);
+ } else {
+ return layoutInflater.inflate(R.layout.fragment_composer_image_frag, viewGroup, false);
+ }
+ } else if (hasSubject) {
+ return layoutInflater.inflate(R.layout.fragment_composer_text_frag, viewGroup, false);
+ } else {
+ return layoutInflater.inflate(R.layout.fragment_composer_frag, viewGroup, false);
+ }
+ } else if (hasImage) {
+ if (hasSubject) {
+ return layoutInflater.inflate(R.layout.fragment_composer_text_image, viewGroup, false);
+ } else {
+ return layoutInflater.inflate(R.layout.fragment_composer_image, viewGroup, false);
+ }
+ } else {
+ return layoutInflater.inflate(R.layout.fragment_composer_text, viewGroup, false);
+ }
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle bundle) {
+ super.onViewCreated(view, bundle);
+ TextView messageText = (TextView) view.findViewById(R.id.answer_message_text);
+ if (messageText != null) {
+ messageText.setText(getSubject());
+ }
+ ImageView mainImage = (ImageView) view.findViewById(R.id.answer_message_image);
+ if (mainImage != null) {
+ Glide.with(this)
+ .load(getImageUri())
+ .transition(DrawableTransitionOptions.withCrossFade())
+ .listener(
+ new RequestListener<Drawable>() {
+ @Override
+ public boolean onLoadFailed(
+ @Nullable GlideException e,
+ Object model,
+ Target<Drawable> target,
+ boolean isFirstResource) {
+ view.findViewById(R.id.loading_spinner).setVisibility(View.GONE);
+ LogUtil.e("MultimediaFragment.onLoadFailed", null, e);
+ // TODO(b/34720074) handle error cases nicely
+ return false; // Let Glide handle the rest
+ }
+
+ @Override
+ public boolean onResourceReady(
+ Drawable drawable,
+ Object model,
+ Target<Drawable> target,
+ DataSource dataSource,
+ boolean isFirstResource) {
+ view.findViewById(R.id.loading_spinner).setVisibility(View.GONE);
+ return false;
+ }
+ })
+ .into(mainImage);
+ mainImage.setClipToOutline(true);
+ }
+ FrameLayout fragmentHolder = (FrameLayout) view.findViewById(R.id.answer_message_frag);
+ if (fragmentHolder != null) {
+ fragmentHolder.setClipToOutline(true);
+ Fragment mapFragment =
+ Assert.isNotNull(mapFactory).getStaticMap(Assert.isNotNull(getLocation()));
+ getChildFragmentManager()
+ .beginTransaction()
+ .replace(R.id.answer_message_frag, mapFragment)
+ .commitNow();
+ }
+ avatarImageView = ((ImageView) view.findViewById(R.id.answer_message_avatar));
+ avatarImageView.setVisibility(showAvatar ? View.VISIBLE : View.GONE);
+
+ Holder parent = FragmentUtils.getParent(this, Holder.class);
+ if (parent != null) {
+ parent.updateAvatar(this);
+ }
+ }
+
+ @Nullable
+ @Override
+ public ImageView getAvatarImageView() {
+ return avatarImageView;
+ }
+
+ @Override
+ public int getAvatarSize() {
+ return getResources().getDimensionPixelSize(R.dimen.answer_message_avatar_size);
+ }
+
+ @Override
+ public boolean shouldShowAnonymousAvatar() {
+ return showAvatar;
+ }
+
+ @Nullable
+ public String getSubject() {
+ return getArguments().getString(ARG_SUBJECT);
+ }
+
+ @Nullable
+ public Uri getImageUri() {
+ return getArguments().getParcelable(ARG_IMAGE);
+ }
+
+ @Nullable
+ public Location getLocation() {
+ return getArguments().getParcelable(ARG_LOCATION);
+ }
+
+ /** Interface for notifying the fragment parent of changes. */
+ public interface Holder {
+ void updateAvatar(AvatarPresenter sessionDataScreen);
+ }
+}
diff --git a/java/com/android/incallui/sessiondata/res/drawable/answer_data_background.xml b/java/com/android/incallui/sessiondata/res/drawable/answer_data_background.xml
new file mode 100644
index 000000000..8826f904b
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/drawable/answer_data_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="16dp"/>
+ <solid android:color="@android:color/white"/>
+</shape>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_frag.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_frag.xml
new file mode 100644
index 000000000..ed2bee0d1
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_frag.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="24dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@id/answer_message_avatar"
+ android:layout_width="@dimen/answer_message_avatar_size"
+ android:layout_height="@dimen/answer_message_avatar_size"
+ android:elevation="@dimen/answer_data_elevation"/>
+
+ <FrameLayout
+ android:id="@id/answer_message_frag"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginBottom="4dp"
+ android:layout_marginStart="8dp"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:outlineProvider="background"/>
+</LinearLayout>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml
new file mode 100644
index 000000000..7000f83b5
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="24dp">
+
+ <ImageView
+ android:id="@id/answer_message_avatar"
+ android:layout_width="@dimen/answer_message_avatar_size"
+ android:layout_height="@dimen/answer_message_avatar_size"
+ android:elevation="@dimen/answer_data_elevation"/>
+
+ <ImageView
+ android:id="@id/answer_message_image"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="4dp"
+ android:layout_marginStart="8dp"
+ android:layout_centerInParent="true"
+ android:layout_toEndOf="@+id/answer_message_avatar"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:outlineProvider="background"
+ android:adjustViewBounds="true"
+ android:scaleType="fitXY"/>
+
+ <ProgressBar
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/loading_spinner"
+ android:layout_centerInParent="true"/>
+</RelativeLayout>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml
new file mode 100644
index 000000000..9959f4dcc
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="24dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@id/answer_message_avatar"
+ android:layout_width="@dimen/answer_message_avatar_size"
+ android:layout_height="@dimen/answer_message_avatar_size"
+ android:layout_rowSpan="2"
+ android:elevation="@dimen/answer_data_elevation"/>
+
+ <ImageView
+ android:id="@id/answer_message_image"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginStart="8dp"
+ android:layout_columnWeight="1"
+ android:layout_rowWeight="1"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:outlineProvider="background"
+ android:scaleType="centerCrop"/>
+
+ <FrameLayout
+ android:id="@id/answer_message_frag"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
+ android:layout_marginStart="8dp"
+ android:layout_column="1"
+ android:layout_columnWeight="1"
+ android:layout_row="1"
+ android:layout_rowWeight="1"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:outlineProvider="background"/>
+</GridLayout>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text.xml
new file mode 100644
index 000000000..c69973042
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="24dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@id/answer_message_avatar"
+ android:layout_width="@dimen/answer_message_avatar_size"
+ android:layout_height="@dimen/answer_message_avatar_size"
+ android:elevation="@dimen/answer_data_elevation"/>
+
+ <TextView
+ android:id="@id/answer_message_text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginBottom="4dp"
+ android:layout_marginStart="8dp"
+ android:padding="18dp"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance.Message"/>
+</LinearLayout>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_frag.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_frag.xml
new file mode 100644
index 000000000..5a1cf728b
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_frag.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="24dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@id/answer_message_avatar"
+ android:layout_width="@dimen/answer_message_avatar_size"
+ android:layout_height="@dimen/answer_message_avatar_size"
+ android:layout_rowSpan="2"
+ android:elevation="@dimen/answer_data_elevation"/>
+
+ <TextView
+ android:id="@id/answer_message_text"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginStart="8dp"
+ android:layout_columnWeight="1"
+ android:layout_rowWeight="1"
+ android:padding="18dp"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:gravity="center_vertical"
+ android:maxLines="2"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance.Message"/>
+
+ <FrameLayout
+ android:id="@id/answer_message_frag"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
+ android:layout_marginStart="8dp"
+ android:layout_column="1"
+ android:layout_columnWeight="1"
+ android:layout_row="1"
+ android:layout_rowWeight="1"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:outlineProvider="background"/>
+</GridLayout>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml
new file mode 100644
index 000000000..995565455
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="24dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@id/answer_message_avatar"
+ android:layout_width="@dimen/answer_message_avatar_size"
+ android:layout_height="@dimen/answer_message_avatar_size"
+ android:layout_rowSpan="2"
+ android:elevation="@dimen/answer_data_elevation"/>
+
+ <TextView
+ android:id="@id/answer_message_text"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginStart="8dp"
+ android:layout_columnWeight="1"
+ android:layout_rowWeight="1"
+ android:padding="18dp"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:gravity="center_vertical"
+ android:maxLines="2"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance.Message"/>
+
+ <ImageView
+ android:id="@id/answer_message_image"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
+ android:layout_marginStart="8dp"
+ android:layout_column="1"
+ android:layout_columnWeight="1"
+ android:layout_row="1"
+ android:layout_rowWeight="1"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:outlineProvider="background"
+ android:scaleType="centerCrop"/>
+</GridLayout>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml
new file mode 100644
index 000000000..387c5cf68
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="24dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@id/answer_message_avatar"
+ android:layout_width="@dimen/answer_message_avatar_size"
+ android:layout_height="@dimen/answer_message_avatar_size"
+ android:layout_rowSpan="2"
+ android:elevation="@dimen/answer_data_elevation"/>
+
+ <TextView
+ android:id="@id/answer_message_text"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginStart="8dp"
+ android:layout_columnWeight="2"
+ android:layout_columnSpan="2"
+ android:layout_rowWeight="1"
+ android:padding="18dp"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:gravity="center_vertical"
+ android:maxLines="2"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance.Message"/>
+
+ <ImageView
+ android:id="@id/answer_message_image"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
+ android:layout_marginStart="8dp"
+ android:layout_column="1"
+ android:layout_columnWeight="1"
+ android:layout_row="1"
+ android:layout_rowWeight="1"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:outlineProvider="background"
+ android:scaleType="centerCrop"/>
+
+ <FrameLayout
+ android:id="@id/answer_message_frag"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
+ android:layout_marginStart="8dp"
+ android:layout_column="2"
+ android:layout_columnWeight="1"
+ android:layout_row="1"
+ android:layout_rowWeight="1"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:outlineProvider="background"/>
+</GridLayout>
diff --git a/java/com/android/incallui/sessiondata/res/values/dimens.xml b/java/com/android/incallui/sessiondata/res/values/dimens.xml
new file mode 100644
index 000000000..76c7edb1b
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/values/dimens.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <dimen name="answer_message_avatar_size">40dp</dimen>
+ <dimen name="answer_data_elevation">2dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/sessiondata/res/values/ids.xml b/java/com/android/incallui/sessiondata/res/values/ids.xml
new file mode 100644
index 000000000..077474c81
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/values/ids.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <item name="answer_message_avatar" type="id"/>
+ <item name="answer_message_text" type="id"/>
+ <item name="answer_message_image" type="id"/>
+ <item name="answer_message_frag" type="id"/>
+</resources>
diff --git a/java/com/android/incallui/sessiondata/res/values/styles.xml b/java/com/android/incallui/sessiondata/res/values/styles.xml
new file mode 100644
index 000000000..dd898a4e2
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/values/styles.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <style name="Dialer.Incall.TextAppearance.Message" parent="Dialer.Incall.TextAppearance">
+ <item name="android:fontFamily">sans-serif</item>
+ <item name="android:textColor">@android:color/black</item>
+ <item name="android:textSize">24sp</item>
+ </style>
+</resources>
diff --git a/java/com/android/incallui/spam/NumberInCallHistoryTask.java b/java/com/android/incallui/spam/NumberInCallHistoryTask.java
new file mode 100644
index 000000000..a225606f6
--- /dev/null
+++ b/java/com/android/incallui/spam/NumberInCallHistoryTask.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.spam;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.os.AsyncTask;
+import android.os.Build.VERSION_CODES;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.support.annotation.NonNull;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import com.android.dialer.common.AsyncTaskExecutor;
+import com.android.dialer.common.AsyncTaskExecutors;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.util.PermissionsUtil;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.CallHistoryStatus;
+import java.util.Objects;
+
+/** Checks if the number is in the call history. */
+@TargetApi(VERSION_CODES.M)
+public class NumberInCallHistoryTask extends AsyncTask<Void, Void, Integer> {
+
+ public static final String TASK_ID = "number_in_call_history_status";
+
+ private final Context context;
+ private final Listener listener;
+ private final String number;
+ private final String countryIso;
+
+ public NumberInCallHistoryTask(
+ @NonNull Context context, @NonNull Listener listener, String number, String countryIso) {
+ this.context = Objects.requireNonNull(context);
+ this.listener = Objects.requireNonNull(listener);
+ this.number = number;
+ this.countryIso = countryIso;
+ }
+
+ public void submitTask() {
+ if (!PermissionsUtil.hasPhonePermissions(context)) {
+ return;
+ }
+ AsyncTaskExecutor asyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor();
+ asyncTaskExecutor.submit(TASK_ID, this);
+ }
+
+ @Override
+ @CallHistoryStatus
+ public Integer doInBackground(Void... params) {
+ String numberToQuery = number;
+ String fieldToQuery = Calls.NUMBER;
+ String normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+
+ // If we can normalize the number successfully, look in "normalized_number"
+ // field instead. Otherwise, look for number in "number" field.
+ if (!TextUtils.isEmpty(normalizedNumber)) {
+ numberToQuery = normalizedNumber;
+ fieldToQuery = Calls.CACHED_NORMALIZED_NUMBER;
+ }
+ try (Cursor cursor =
+ context
+ .getContentResolver()
+ .query(
+ TelecomUtil.getCallLogUri(context),
+ new String[] {CallLog.Calls._ID},
+ fieldToQuery + " = ?",
+ new String[] {numberToQuery},
+ null)) {
+ return cursor != null && cursor.getCount() > 0
+ ? DialerCall.CALL_HISTORY_STATUS_PRESENT
+ : DialerCall.CALL_HISTORY_STATUS_NOT_PRESENT;
+ } catch (SQLiteException e) {
+ LogUtil.e("NumberInCallHistoryTask.doInBackground", "query call log error", e);
+ return DialerCall.CALL_HISTORY_STATUS_UNKNOWN;
+ }
+ }
+
+ @Override
+ public void onPostExecute(@CallHistoryStatus Integer callHistoryStatus) {
+ listener.onComplete(callHistoryStatus);
+ }
+
+ /** Callback for the async task. */
+ public interface Listener {
+
+ void onComplete(@CallHistoryStatus int callHistoryStatus);
+ }
+}
diff --git a/java/com/android/incallui/spam/SpamCallListListener.java b/java/com/android/incallui/spam/SpamCallListListener.java
new file mode 100644
index 000000000..0897842de
--- /dev/null
+++ b/java/com/android/incallui/spam/SpamCallListListener.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.spam;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.telecom.DisconnectCause;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.dialer.blocking.FilteredNumberCompat;
+import com.android.dialer.blocking.FilteredNumbersUtil;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.ContactLookupResult;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.spam.Spam;
+import com.android.incallui.R;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.CallHistoryStatus;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import java.util.Random;
+
+/**
+ * Creates notifications after a call ends if the call matched the criteria (incoming, accepted,
+ * etc).
+ */
+public class SpamCallListListener implements CallList.Listener {
+
+ static final int NOTIFICATION_ID = 1;
+ private static final String TAG = "SpamCallListListener";
+ private final Context context;
+ private final Random random;
+
+ public SpamCallListListener(Context context) {
+ this.context = context;
+ this.random = new Random();
+ }
+
+ public SpamCallListListener(Context context, Random rand) {
+ this.context = context;
+ this.random = rand;
+ }
+
+ private static String pii(String pii) {
+ return com.android.incallui.Log.pii(pii);
+ }
+
+ @Override
+ public void onIncomingCall(final DialerCall call) {
+ String number = call.getNumber();
+ if (TextUtils.isEmpty(number)) {
+ return;
+ }
+ NumberInCallHistoryTask.Listener listener =
+ new NumberInCallHistoryTask.Listener() {
+ @Override
+ public void onComplete(@CallHistoryStatus int callHistoryStatus) {
+ call.setCallHistoryStatus(callHistoryStatus);
+ }
+ };
+ new NumberInCallHistoryTask(context, listener, number, GeoUtil.getCurrentCountryIso(context))
+ .submitTask();
+ }
+
+ @Override
+ public void onUpgradeToVideo(DialerCall call) {}
+
+ @Override
+ public void onSessionModificationStateChange(@SessionModificationState int newState) {}
+
+ @Override
+ public void onCallListChange(CallList callList) {}
+
+ @Override
+ public void onWiFiToLteHandover(DialerCall call) {}
+
+ @Override
+ public void onHandoverToWifiFailed(DialerCall call) {}
+
+ @Override
+ public void onDisconnect(DialerCall call) {
+ if (!shouldShowAfterCallNotification(call)) {
+ return;
+ }
+ String e164Number =
+ PhoneNumberUtils.formatNumberToE164(
+ call.getNumber(), GeoUtil.getCurrentCountryIso(context));
+ if (!FilteredNumbersUtil.canBlockNumber(context, e164Number, call.getNumber())
+ || !FilteredNumberCompat.canAttemptBlockOperations(context)) {
+ return;
+ }
+ if (e164Number == null) {
+ return;
+ }
+ showNotification(call);
+ }
+
+ /** Posts the intent for displaying the after call spam notification to the user. */
+ private void showNotification(DialerCall call) {
+ if (call.isSpam()) {
+ maybeShowSpamCallNotification(call);
+ } else {
+ LogUtil.d(TAG, "Showing not spam notification for number=" + pii(call.getNumber()));
+ maybeShowNonSpamCallNotification(call);
+ }
+ }
+
+ /** Determines if the after call notification should be shown for the specified call. */
+ private boolean shouldShowAfterCallNotification(DialerCall call) {
+ if (!Spam.get(context).isSpamNotificationEnabled()) {
+ return false;
+ }
+
+ String number = call.getNumber();
+ if (TextUtils.isEmpty(number)) {
+ return false;
+ }
+
+ DialerCall.LogState logState = call.getLogState();
+ if (!logState.isIncoming) {
+ return false;
+ }
+
+ if (logState.duration <= 0) {
+ return false;
+ }
+
+ if (logState.contactLookupResult != ContactLookupResult.Type.NOT_FOUND
+ && logState.contactLookupResult != ContactLookupResult.Type.UNKNOWN_LOOKUP_RESULT_TYPE) {
+ return false;
+ }
+
+ int callHistoryStatus = call.getCallHistoryStatus();
+ if (callHistoryStatus == DialerCall.CALL_HISTORY_STATUS_PRESENT) {
+ return false;
+ } else if (callHistoryStatus == DialerCall.CALL_HISTORY_STATUS_UNKNOWN) {
+ LogUtil.i(TAG, "DialerCall history status is unknown, returning false");
+ return false;
+ }
+
+ // Check if call disconnected because of either user hanging up
+ int disconnectCause = call.getDisconnectCause().getCode();
+ if (disconnectCause != DisconnectCause.LOCAL && disconnectCause != DisconnectCause.REMOTE) {
+ return false;
+ }
+
+ LogUtil.i(TAG, "shouldShowAfterCallNotification, returning true");
+ return true;
+ }
+
+ /**
+ * Creates a notification builder with properties common among the two after call notifications.
+ */
+ private Notification.Builder createAfterCallNotificationBuilder(DialerCall call) {
+ return new Notification.Builder(context)
+ .setContentIntent(
+ createActivityPendingIntent(call, SpamNotificationActivity.ACTION_SHOW_DIALOG))
+ .setCategory(Notification.CATEGORY_STATUS)
+ .setPriority(Notification.PRIORITY_DEFAULT)
+ .setColor(context.getColor(R.color.dialer_theme_color))
+ .setSmallIcon(R.drawable.ic_call_end_white_24dp);
+ }
+
+ private CharSequence getDisplayNumber(DialerCall call) {
+ String formattedNumber =
+ PhoneNumberUtils.formatNumber(call.getNumber(), GeoUtil.getCurrentCountryIso(context));
+ return PhoneNumberUtilsCompat.createTtsSpannable(formattedNumber);
+ }
+
+ /** Display a notification with two actions: "add contact" and "report spam". */
+ private void showNonSpamCallNotification(DialerCall call) {
+ Notification.Builder notificationBuilder =
+ createAfterCallNotificationBuilder(call)
+ .setLargeIcon(Icon.createWithResource(context, R.drawable.unknown_notification_icon))
+ .setContentText(
+ context.getString(R.string.spam_notification_non_spam_call_collapsed_text))
+ .setStyle(
+ new Notification.BigTextStyle()
+ .bigText(
+ context.getString(R.string.spam_notification_non_spam_call_expanded_text)))
+ // Add contact
+ .addAction(
+ new Notification.Action.Builder(
+ R.drawable.ic_person_add_grey600_24dp,
+ context.getString(R.string.spam_notification_add_contact_action_text),
+ createActivityPendingIntent(
+ call, SpamNotificationActivity.ACTION_ADD_TO_CONTACTS))
+ .build())
+ // Block/report spam
+ .addAction(
+ new Notification.Action.Builder(
+ R.drawable.ic_block_grey600_24dp,
+ context.getString(R.string.spam_notification_report_spam_action_text),
+ createBlockReportSpamPendingIntent(call))
+ .build())
+ .setContentTitle(
+ context.getString(R.string.non_spam_notification_title, getDisplayNumber(call)));
+ ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
+ .notify(call.getNumber(), NOTIFICATION_ID, notificationBuilder.build());
+ }
+
+ private boolean shouldThrottleSpamNotification() {
+ int randomNumber = random.nextInt(100);
+ int thresholdForShowing = Spam.get(context).percentOfSpamNotificationsToShow();
+ if (thresholdForShowing == 0) {
+ LogUtil.d(
+ TAG,
+ "shouldThrottleSpamNotification, not showing - percentOfSpamNotificationsToShow is 0");
+ return true;
+ } else if (randomNumber < thresholdForShowing) {
+ LogUtil.d(
+ TAG,
+ "shouldThrottleSpamNotification, showing " + randomNumber + " < " + thresholdForShowing);
+ return false;
+ } else {
+ LogUtil.d(
+ TAG,
+ "shouldThrottleSpamNotification, not showing "
+ + randomNumber
+ + " >= "
+ + thresholdForShowing);
+ return true;
+ }
+ }
+
+ private boolean shouldThrottleNonSpamNotification() {
+ int randomNumber = random.nextInt(100);
+ int thresholdForShowing = Spam.get(context).percentOfNonSpamNotificationsToShow();
+ if (thresholdForShowing == 0) {
+ LogUtil.d(TAG, "Not showing non spam notification: percentOfNonSpamNotificationsToShow is 0");
+ return true;
+ } else if (randomNumber < thresholdForShowing) {
+ LogUtil.d(
+ TAG, "Showing non spam notification: " + randomNumber + " < " + thresholdForShowing);
+ return false;
+ } else {
+ LogUtil.d(
+ TAG, "Not showing non spam notification:" + randomNumber + " >= " + thresholdForShowing);
+ return true;
+ }
+ }
+
+ private void maybeShowSpamCallNotification(DialerCall call) {
+ if (shouldThrottleSpamNotification()) {
+ Logger.get(context)
+ .logCallImpression(
+ DialerImpression.Type.SPAM_NOTIFICATION_NOT_SHOWN_AFTER_THROTTLE,
+ call.getUniqueCallId(),
+ call.getTimeAddedMs());
+ } else {
+ Logger.get(context)
+ .logCallImpression(
+ DialerImpression.Type.SPAM_NOTIFICATION_SHOWN_AFTER_THROTTLE,
+ call.getUniqueCallId(),
+ call.getTimeAddedMs());
+ showSpamCallNotification(call);
+ }
+ }
+
+ private void maybeShowNonSpamCallNotification(DialerCall call) {
+ if (shouldThrottleNonSpamNotification()) {
+ Logger.get(context)
+ .logCallImpression(
+ DialerImpression.Type.NON_SPAM_NOTIFICATION_NOT_SHOWN_AFTER_THROTTLE,
+ call.getUniqueCallId(),
+ call.getTimeAddedMs());
+ } else {
+ Logger.get(context)
+ .logCallImpression(
+ DialerImpression.Type.NON_SPAM_NOTIFICATION_SHOWN_AFTER_THROTTLE,
+ call.getUniqueCallId(),
+ call.getTimeAddedMs());
+ showNonSpamCallNotification(call);
+ }
+ }
+
+ /** Display a notification with the action "not spam". */
+ private void showSpamCallNotification(DialerCall call) {
+ Notification.Builder notificationBuilder =
+ createAfterCallNotificationBuilder(call)
+ .setLargeIcon(Icon.createWithResource(context, R.drawable.spam_notification_icon))
+ .setContentText(context.getString(R.string.spam_notification_spam_call_collapsed_text))
+ .setStyle(
+ new Notification.BigTextStyle()
+ .bigText(context.getString(R.string.spam_notification_spam_call_expanded_text)))
+ // Not spam
+ .addAction(
+ new Notification.Action.Builder(
+ R.drawable.ic_close_grey600_24dp,
+ context.getString(R.string.spam_notification_not_spam_action_text),
+ createNotSpamPendingIntent(call))
+ .build())
+ // Block/report spam
+ .addAction(
+ new Notification.Action.Builder(
+ R.drawable.ic_block_grey600_24dp,
+ context.getString(R.string.spam_notification_block_spam_action_text),
+ createBlockReportSpamPendingIntent(call))
+ .build())
+ .setContentTitle(
+ context.getString(R.string.spam_notification_title, getDisplayNumber(call)));
+ ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
+ .notify(call.getNumber(), NOTIFICATION_ID, notificationBuilder.build());
+ }
+
+ /**
+ * Creates a pending intent for block/report spam action. If enabled, this intent is forwarded to
+ * the {@link SpamNotificationActivity}, otherwise to the {@link SpamNotificationService}.
+ */
+ private PendingIntent createBlockReportSpamPendingIntent(DialerCall call) {
+ String action = SpamNotificationActivity.ACTION_MARK_NUMBER_AS_SPAM;
+ return Spam.get(context).isDialogEnabledForSpamNotification()
+ ? createActivityPendingIntent(call, action)
+ : createServicePendingIntent(call, action);
+ }
+
+ /**
+ * Creates a pending intent for not spam action. If enabled, this intent is forwarded to the
+ * {@link SpamNotificationActivity}, otherwise to the {@link SpamNotificationService}.
+ */
+ private PendingIntent createNotSpamPendingIntent(DialerCall call) {
+ String action = SpamNotificationActivity.ACTION_MARK_NUMBER_AS_NOT_SPAM;
+ return Spam.get(context).isDialogEnabledForSpamNotification()
+ ? createActivityPendingIntent(call, action)
+ : createServicePendingIntent(call, action);
+ }
+
+ /** Creates a pending intent for {@link SpamNotificationService}. */
+ private PendingIntent createServicePendingIntent(DialerCall call, String action) {
+ Intent intent =
+ SpamNotificationService.createServiceIntent(context, call, action, NOTIFICATION_ID);
+ return PendingIntent.getService(
+ context, (int) System.currentTimeMillis(), intent, PendingIntent.FLAG_ONE_SHOT);
+ }
+
+ /** Creates a pending intent for {@link SpamNotificationActivity}. */
+ private PendingIntent createActivityPendingIntent(DialerCall call, String action) {
+ Intent intent =
+ SpamNotificationActivity.createActivityIntent(context, call, action, NOTIFICATION_ID);
+ return PendingIntent.getActivity(
+ context, (int) System.currentTimeMillis(), intent, PendingIntent.FLAG_ONE_SHOT);
+ }
+}
diff --git a/java/com/android/incallui/spam/SpamNotificationActivity.java b/java/com/android/incallui/spam/SpamNotificationActivity.java
new file mode 100644
index 000000000..88d6bdfda
--- /dev/null
+++ b/java/com/android/incallui/spam/SpamNotificationActivity.java
@@ -0,0 +1,483 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.spam;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.CallLog;
+import android.provider.ContactsContract;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.FragmentActivity;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.dialer.blocking.BlockReportSpamDialogs;
+import com.android.dialer.blocking.BlockedNumbersMigrator;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.blocking.FilteredNumberCompat;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.logging.nano.ReportingLocation;
+import com.android.dialer.spam.Spam;
+import com.android.incallui.R;
+import com.android.incallui.call.DialerCall;
+
+/** Creates the after call notification dialogs. */
+public class SpamNotificationActivity extends FragmentActivity {
+
+ /** Action to add number to contacts. */
+ static final String ACTION_ADD_TO_CONTACTS = "com.android.incallui.spam.ACTION_ADD_TO_CONTACTS";
+ /** Action to show dialog. */
+ static final String ACTION_SHOW_DIALOG = "com.android.incallui.spam.ACTION_SHOW_DIALOG";
+ /** Action to mark a number as spam. */
+ static final String ACTION_MARK_NUMBER_AS_SPAM =
+ "com.android.incallui.spam.ACTION_MARK_NUMBER_AS_SPAM";
+ /** Action to mark a number as not spam. */
+ static final String ACTION_MARK_NUMBER_AS_NOT_SPAM =
+ "com.android.incallui.spam.ACTION_MARK_NUMBER_AS_NOT_SPAM";
+
+ private static final String TAG = "SpamNotifications";
+ private static final String EXTRA_NOTIFICATION_ID = "notification_id";
+ private static final String EXTRA_CALL_INFO = "call_info";
+
+ private static final String CALL_INFO_KEY_PHONE_NUMBER = "phone_number";
+ private static final String CALL_INFO_KEY_IS_SPAM = "is_spam";
+ private static final String CALL_INFO_KEY_CALL_ID = "call_id";
+ private static final String CALL_INFO_KEY_START_TIME_MILLIS = "call_start_time_millis";
+ private static final String CALL_INFO_CONTACT_LOOKUP_RESULT_TYPE = "contact_lookup_result_type";
+ private final DialogInterface.OnDismissListener dismissListener =
+ new DialogInterface.OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ if (!isFinishing()) {
+ finish();
+ }
+ }
+ };
+ private FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler;
+
+ /**
+ * Creates an intent to start this activity.
+ *
+ * @return Intent intent that starts this activity.
+ */
+ public static Intent createActivityIntent(
+ Context context, DialerCall call, String action, int notificationId) {
+ Intent intent = new Intent(context, SpamNotificationActivity.class);
+ intent.setAction(action);
+ // This ensures only one activity of this kind exists at a time.
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(EXTRA_NOTIFICATION_ID, notificationId);
+ intent.putExtra(EXTRA_CALL_INFO, newCallInfoBundle(call));
+ return intent;
+ }
+
+ /** Creates the intent to insert a contact. */
+ private static Intent createInsertContactsIntent(String number) {
+ Intent intent = new Intent(ContactsContract.Intents.Insert.ACTION);
+ // This ensures that the edit contact number field gets updated if called more than once.
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.setType(ContactsContract.RawContacts.CONTENT_TYPE);
+ intent.putExtra(ContactsContract.Intents.Insert.PHONE, number);
+ return intent;
+ }
+
+ /** Returns the formatted version of the given number. */
+ private static String getFormattedNumber(String number) {
+ return PhoneNumberUtilsCompat.createTtsSpannable(number).toString();
+ }
+
+ private static void logCallImpression(Context context, Bundle bundle, int impression) {
+ Logger.get(context)
+ .logCallImpression(
+ impression,
+ bundle.getString(CALL_INFO_KEY_CALL_ID),
+ bundle.getLong(CALL_INFO_KEY_START_TIME_MILLIS, 0));
+ }
+
+ private static Bundle newCallInfoBundle(DialerCall call) {
+ Bundle bundle = new Bundle();
+ bundle.putString(CALL_INFO_KEY_PHONE_NUMBER, call.getNumber());
+ bundle.putBoolean(CALL_INFO_KEY_IS_SPAM, call.isSpam());
+ bundle.putString(CALL_INFO_KEY_CALL_ID, call.getUniqueCallId());
+ bundle.putLong(CALL_INFO_KEY_START_TIME_MILLIS, call.getTimeAddedMs());
+ bundle.putInt(CALL_INFO_CONTACT_LOOKUP_RESULT_TYPE, call.getLogState().contactLookupResult);
+ return bundle;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ LogUtil.i(TAG, "onCreate");
+ super.onCreate(savedInstanceState);
+ setFinishOnTouchOutside(true);
+ filteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(this);
+ cancelNotification();
+ }
+
+ @Override
+ protected void onResume() {
+ LogUtil.i(TAG, "onResume");
+ super.onResume();
+ Intent intent = getIntent();
+ String number = getCallInfo().getString(CALL_INFO_KEY_PHONE_NUMBER);
+ boolean isSpam = getCallInfo().getBoolean(CALL_INFO_KEY_IS_SPAM);
+ int contactLookupResultType = getCallInfo().getInt(CALL_INFO_CONTACT_LOOKUP_RESULT_TYPE, 0);
+ switch (intent.getAction()) {
+ case ACTION_ADD_TO_CONTACTS:
+ logCallImpression(DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_ADD_TO_CONTACTS);
+ startActivity(createInsertContactsIntent(number));
+ finish();
+ break;
+ case ACTION_MARK_NUMBER_AS_SPAM:
+ assertDialogsEnabled();
+ maybeShowBlockReportSpamDialog(number, contactLookupResultType);
+ break;
+ case ACTION_MARK_NUMBER_AS_NOT_SPAM:
+ assertDialogsEnabled();
+ maybeShowNotSpamDialog(number, contactLookupResultType);
+ break;
+ case ACTION_SHOW_DIALOG:
+ if (isSpam) {
+ showSpamFullDialog();
+ } else {
+ showNonSpamDialog();
+ }
+ break;
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ LogUtil.d(TAG, "onPause");
+ // Finish activity on pause (e.g: orientation change or back button pressed)
+ filteredNumberAsyncQueryHandler = null;
+ if (!isFinishing()) {
+ finish();
+ }
+ super.onPause();
+ }
+
+ /** Creates and displays the dialog for whitelisting a number. */
+ private void maybeShowNotSpamDialog(final String number, final int contactLookupResultType) {
+ if (Spam.get(this).isDialogEnabledForSpamNotification()) {
+ BlockReportSpamDialogs.ReportNotSpamDialogFragment.newInstance(
+ getFormattedNumber(number),
+ new BlockReportSpamDialogs.OnConfirmListener() {
+ @Override
+ public void onClick() {
+ reportNotSpamAndFinish(number, contactLookupResultType);
+ }
+ },
+ dismissListener)
+ .show(getFragmentManager(), BlockReportSpamDialogs.NOT_SPAM_DIALOG_TAG);
+ } else {
+ reportNotSpamAndFinish(number, contactLookupResultType);
+ }
+ }
+
+ /** Creates and displays the dialog for blocking/reporting a number as spam. */
+ private void maybeShowBlockReportSpamDialog(
+ final String number, final int contactLookupResultType) {
+ if (Spam.get(this).isDialogEnabledForSpamNotification()) {
+ maybeShowBlockNumberMigrationDialog(
+ new BlockedNumbersMigrator.Listener() {
+ @Override
+ public void onComplete() {
+ BlockReportSpamDialogs.BlockReportSpamDialogFragment.newInstance(
+ getFormattedNumber(number),
+ Spam.get(SpamNotificationActivity.this).isDialogReportSpamCheckedByDefault(),
+ new BlockReportSpamDialogs.OnSpamDialogClickListener() {
+ @Override
+ public void onClick(boolean isSpamChecked) {
+ blockReportNumberAndFinish(
+ number, isSpamChecked, contactLookupResultType);
+ }
+ },
+ dismissListener)
+ .show(getFragmentManager(), BlockReportSpamDialogs.BLOCK_REPORT_SPAM_DIALOG_TAG);
+ }
+ });
+ } else {
+ blockReportNumberAndFinish(number, true, contactLookupResultType);
+ }
+ }
+
+ /**
+ * Displays the dialog for the first time unknown calls with actions "Add contact", "Block/report
+ * spam", and "Dismiss".
+ */
+ private void showNonSpamDialog() {
+ logCallImpression(DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_SHOW_NON_SPAM_DIALOG);
+ FirstTimeNonSpamCallDialogFragment.newInstance(getCallInfo())
+ .show(getSupportFragmentManager(), FirstTimeNonSpamCallDialogFragment.TAG);
+ }
+
+ /**
+ * Displays the dialog for first time spam calls with actions "Not spam", "Block", and "Dismiss".
+ */
+ private void showSpamFullDialog() {
+ logCallImpression(DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_SHOW_SPAM_DIALOG);
+ FirstTimeSpamCallDialogFragment.newInstance(getCallInfo())
+ .show(getSupportFragmentManager(), FirstTimeSpamCallDialogFragment.TAG);
+ }
+
+ /** Checks if the user has migrated to the new blocking and display a dialog if necessary. */
+ private void maybeShowBlockNumberMigrationDialog(BlockedNumbersMigrator.Listener listener) {
+ if (!FilteredNumberCompat.maybeShowBlockNumberMigrationDialog(
+ this, getFragmentManager(), listener)) {
+ listener.onComplete();
+ }
+ }
+
+ /** Block and report the number as spam. */
+ private void blockReportNumberAndFinish(
+ String number, boolean reportAsSpam, int contactLookupResultType) {
+ if (reportAsSpam) {
+ logCallImpression(DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_SPAM);
+ Spam.get(this)
+ .reportSpamFromAfterCallNotification(
+ number,
+ getCountryIso(),
+ CallLog.Calls.INCOMING_TYPE,
+ ReportingLocation.Type.FEEDBACK_PROMPT,
+ contactLookupResultType);
+ }
+
+ logCallImpression(DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_BLOCK_NUMBER);
+ filteredNumberAsyncQueryHandler.blockNumber(null, number, getCountryIso());
+ // TODO: DialerCall finish() after block/reporting async tasks complete (b/28441936)
+ finish();
+ }
+
+ /** Report the number as not spam. */
+ private void reportNotSpamAndFinish(String number, int contactLookupResultType) {
+ logCallImpression(DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_REPORT_NUMBER_AS_NOT_SPAM);
+ Spam.get(this)
+ .reportNotSpamFromAfterCallNotification(
+ number,
+ getCountryIso(),
+ CallLog.Calls.INCOMING_TYPE,
+ ReportingLocation.Type.FEEDBACK_PROMPT,
+ contactLookupResultType);
+ // TODO: DialerCall finish() after async task completes (b/28441936)
+ finish();
+ }
+
+ /** Cancels the notification associated with the number. */
+ private void cancelNotification() {
+ int notificationId = getIntent().getIntExtra(EXTRA_NOTIFICATION_ID, 1);
+ String number = getCallInfo().getString(CALL_INFO_KEY_PHONE_NUMBER);
+ ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE))
+ .cancel(number, notificationId);
+ }
+
+ private String getCountryIso() {
+ return GeoUtil.getCurrentCountryIso(this);
+ }
+
+ private void assertDialogsEnabled() {
+ if (!Spam.get(this).isDialogEnabledForSpamNotification()) {
+ throw new IllegalStateException(
+ "Cannot start this activity with given action because dialogs are not enabled.");
+ }
+ }
+
+ private Bundle getCallInfo() {
+ return getIntent().getBundleExtra(EXTRA_CALL_INFO);
+ }
+
+ private void logCallImpression(int impression) {
+ logCallImpression(this, getCallInfo(), impression);
+ }
+
+ /** Dialog that displays "Not spam", "Block/report spam" and "Dismiss". */
+ public static class FirstTimeSpamCallDialogFragment extends DialogFragment {
+
+ public static final String TAG = "FirstTimeSpamDialog";
+
+ private boolean dismissed;
+ private Context applicationContext;
+
+ private static DialogFragment newInstance(Bundle bundle) {
+ FirstTimeSpamCallDialogFragment fragment = new FirstTimeSpamCallDialogFragment();
+ fragment.setArguments(bundle);
+ return fragment;
+ }
+
+ @Override
+ public void onPause() {
+ dismiss();
+ super.onPause();
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ logCallImpression(
+ applicationContext,
+ getArguments(),
+ DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_ON_DISMISS_SPAM_DIALOG);
+ super.onDismiss(dialog);
+ // If dialog was not dismissed by user pressing one of the buttons, finish activity
+ if (!dismissed && getActivity() != null && !getActivity().isFinishing()) {
+ getActivity().finish();
+ }
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ applicationContext = context.getApplicationContext();
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ super.onCreateDialog(savedInstanceState);
+ final SpamNotificationActivity spamNotificationActivity =
+ (SpamNotificationActivity) getActivity();
+ final String number = getArguments().getString(CALL_INFO_KEY_PHONE_NUMBER);
+ final int contactLookupResultType =
+ getArguments().getInt(CALL_INFO_CONTACT_LOOKUP_RESULT_TYPE, 0);
+
+ return new AlertDialog.Builder(getActivity())
+ .setCancelable(false)
+ .setTitle(getString(R.string.spam_notification_title, getFormattedNumber(number)))
+ .setMessage(getString(R.string.spam_notification_spam_call_expanded_text))
+ .setNeutralButton(
+ getString(R.string.notification_action_dismiss),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dismiss();
+ }
+ })
+ .setPositiveButton(
+ getString(R.string.spam_notification_dialog_was_not_spam_action_text),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dismissed = true;
+ dismiss();
+ spamNotificationActivity.maybeShowNotSpamDialog(number, contactLookupResultType);
+ }
+ })
+ .setNegativeButton(
+ getString(R.string.spam_notification_block_spam_action_text),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dismissed = true;
+ dismiss();
+ spamNotificationActivity.maybeShowBlockReportSpamDialog(
+ number, contactLookupResultType);
+ }
+ })
+ .create();
+ }
+ }
+
+ /** Dialog that displays "Add contact", "Block/report spam" and "Dismiss". */
+ public static class FirstTimeNonSpamCallDialogFragment extends DialogFragment {
+
+ public static final String TAG = "FirstTimeNonSpamDialog";
+
+ private boolean dismissed;
+ private Context context;
+
+ private static DialogFragment newInstance(Bundle bundle) {
+ FirstTimeNonSpamCallDialogFragment fragment = new FirstTimeNonSpamCallDialogFragment();
+ fragment.setArguments(bundle);
+ return fragment;
+ }
+
+ @Override
+ public void onPause() {
+ // Dismiss on pause e.g: orientation change
+ dismiss();
+ super.onPause();
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ super.onDismiss(dialog);
+ logCallImpression(
+ context,
+ getArguments(),
+ DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_ON_DISMISS_NON_SPAM_DIALOG);
+ // If dialog was not dismissed by user pressing one of the buttons, finish activity
+ if (!dismissed && getActivity() != null && !getActivity().isFinishing()) {
+ getActivity().finish();
+ }
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ this.context = context.getApplicationContext();
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ super.onCreateDialog(savedInstanceState);
+ final SpamNotificationActivity spamNotificationActivity =
+ (SpamNotificationActivity) getActivity();
+ final String number = getArguments().getString(CALL_INFO_KEY_PHONE_NUMBER);
+ final int contactLookupResultType =
+ getArguments().getInt(CALL_INFO_CONTACT_LOOKUP_RESULT_TYPE, 0);
+ return new AlertDialog.Builder(getActivity())
+ .setTitle(getString(R.string.non_spam_notification_title, getFormattedNumber(number)))
+ .setCancelable(false)
+ .setMessage(getString(R.string.spam_notification_non_spam_call_expanded_text))
+ .setNeutralButton(
+ getString(R.string.notification_action_dismiss),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dismiss();
+ }
+ })
+ .setPositiveButton(
+ getString(R.string.spam_notification_dialog_add_contact_action_text),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dismissed = true;
+ dismiss();
+ startActivity(createInsertContactsIntent(number));
+ }
+ })
+ .setNegativeButton(
+ getString(R.string.spam_notification_dialog_block_report_spam_action_text),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dismissed = true;
+ dismiss();
+ spamNotificationActivity.maybeShowBlockReportSpamDialog(
+ number, contactLookupResultType);
+ }
+ })
+ .create();
+ }
+ }
+}
diff --git a/java/com/android/incallui/spam/SpamNotificationService.java b/java/com/android/incallui/spam/SpamNotificationService.java
new file mode 100644
index 000000000..bf107f789
--- /dev/null
+++ b/java/com/android/incallui/spam/SpamNotificationService.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.spam;
+
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.provider.CallLog;
+import android.support.annotation.Nullable;
+import com.android.contacts.common.GeoUtil;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.logging.nano.ReportingLocation;
+import com.android.dialer.spam.Spam;
+import com.android.incallui.call.DialerCall;
+
+/**
+ * This service determines if the device is locked/unlocked and takes an action based on the state.
+ * A service is used to to determine this, as opposed to an activity, because the user must unlock
+ * the device before a notification can start an activity. This is not the case for a service, and
+ * intents can be sent to this service even from the lock screen. This allows users to quickly
+ * report a number as spam or not spam from their lock screen.
+ */
+public class SpamNotificationService extends Service {
+
+ private static final String TAG = "SpamNotificationSvc";
+
+ private static final String EXTRA_PHONE_NUMBER = "service_phone_number";
+ private static final String EXTRA_CALL_ID = "service_call_id";
+ private static final String EXTRA_CALL_START_TIME_MILLIS = "service_call_start_time_millis";
+ private static final String EXTRA_NOTIFICATION_ID = "service_notification_id";
+ private static final String EXTRA_CONTACT_LOOKUP_RESULT_TYPE =
+ "service_contact_lookup_result_type";
+ /** Creates an intent to start this service. */
+ public static Intent createServiceIntent(
+ Context context, DialerCall call, String action, int notificationId) {
+ Intent intent = new Intent(context, SpamNotificationService.class);
+ intent.setAction(action);
+ intent.putExtra(EXTRA_PHONE_NUMBER, call.getNumber());
+ intent.putExtra(EXTRA_CALL_ID, call.getUniqueCallId());
+ intent.putExtra(EXTRA_CALL_START_TIME_MILLIS, call.getTimeAddedMs());
+ intent.putExtra(EXTRA_NOTIFICATION_ID, notificationId);
+ intent.putExtra(EXTRA_CONTACT_LOOKUP_RESULT_TYPE, call.getLogState().contactLookupResult);
+ return intent;
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ // Return null because clients cannot bind to this service
+ return null;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ LogUtil.d(TAG, "onStartCommand");
+ if (intent == null) {
+ LogUtil.d(TAG, "Null intent");
+ stopSelf();
+ // Return {@link #START_NOT_STICKY} so service is not restarted.
+ return START_NOT_STICKY;
+ }
+ String number = intent.getStringExtra(EXTRA_PHONE_NUMBER);
+ int notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, 1);
+ String countryIso = GeoUtil.getCurrentCountryIso(this);
+ int contactLookupResultType = intent.getIntExtra(EXTRA_CONTACT_LOOKUP_RESULT_TYPE, 0);
+
+ ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE))
+ .cancel(number, notificationId);
+
+ switch (intent.getAction()) {
+ case SpamNotificationActivity.ACTION_MARK_NUMBER_AS_SPAM:
+ logCallImpression(
+ intent, DialerImpression.Type.SPAM_NOTIFICATION_SERVICE_ACTION_MARK_NUMBER_AS_SPAM);
+ Spam.get(this)
+ .reportSpamFromAfterCallNotification(
+ number,
+ countryIso,
+ CallLog.Calls.INCOMING_TYPE,
+ ReportingLocation.Type.FEEDBACK_PROMPT,
+ contactLookupResultType);
+ new FilteredNumberAsyncQueryHandler(this).blockNumber(null, number, countryIso);
+ break;
+ case SpamNotificationActivity.ACTION_MARK_NUMBER_AS_NOT_SPAM:
+ logCallImpression(
+ intent, DialerImpression.Type.SPAM_NOTIFICATION_SERVICE_ACTION_MARK_NUMBER_AS_NOT_SPAM);
+ Spam.get(this)
+ .reportNotSpamFromAfterCallNotification(
+ number,
+ countryIso,
+ CallLog.Calls.INCOMING_TYPE,
+ ReportingLocation.Type.FEEDBACK_PROMPT,
+ contactLookupResultType);
+ break;
+ }
+ // TODO: call stopSelf() after async tasks complete (b/28441936)
+ stopSelf();
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ LogUtil.d(TAG, "onDestroy");
+ }
+
+ private void logCallImpression(Intent intent, int impression) {
+ Logger.get(this)
+ .logCallImpression(
+ impression,
+ intent.getStringExtra(EXTRA_CALL_ID),
+ intent.getLongExtra(EXTRA_CALL_START_TIME_MILLIS, 0));
+ }
+}
diff --git a/java/com/android/incallui/util/AccessibilityUtil.java b/java/com/android/incallui/util/AccessibilityUtil.java
new file mode 100644
index 000000000..65753484a
--- /dev/null
+++ b/java/com/android/incallui/util/AccessibilityUtil.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.util;
+
+import android.content.Context;
+import android.view.accessibility.AccessibilityManager;
+
+public class AccessibilityUtil {
+
+ public static boolean isAccessibilityEnabled(Context context) {
+ AccessibilityManager accessibilityManager =
+ context.getSystemService(AccessibilityManager.class);
+ return accessibilityManager.isEnabled();
+ }
+
+ public static boolean isTouchExplorationEnabled(Context context) {
+ AccessibilityManager accessibilityManager =
+ context.getSystemService(AccessibilityManager.class);
+ return accessibilityManager.isTouchExplorationEnabled();
+ }
+}
diff --git a/java/com/android/incallui/util/TelecomCallUtil.java b/java/com/android/incallui/util/TelecomCallUtil.java
new file mode 100644
index 000000000..8855543b1
--- /dev/null
+++ b/java/com/android/incallui/util/TelecomCallUtil.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.util;
+
+import android.net.Uri;
+import android.telecom.Call;
+import android.telephony.PhoneNumberUtils;
+
+/**
+ * Class to provide a standard interface for obtaining information from the underlying
+ * android.telecom.Call. Much of this should be obtained through the incall.Call, but on occasion we
+ * need to interact with the telecom.Call directly (eg. call blocking, before the incall.Call has
+ * been created).
+ */
+public class TelecomCallUtil {
+
+ // Whether the call handle is an emergency number.
+ public static boolean isEmergencyCall(Call call) {
+ Uri handle = call.getDetails().getHandle();
+ return PhoneNumberUtils.isEmergencyNumber(handle == null ? "" : handle.getSchemeSpecificPart());
+ }
+
+ public static String getNumber(Call call) {
+ if (call == null) {
+ return null;
+ }
+ if (call.getDetails().getGatewayInfo() != null) {
+ return call.getDetails().getGatewayInfo().getOriginalAddress().getSchemeSpecificPart();
+ }
+ Uri handle = getHandle(call);
+ return handle == null ? null : handle.getSchemeSpecificPart();
+ }
+
+ public static Uri getHandle(Call call) {
+ return call == null ? null : call.getDetails().getHandle();
+ }
+}
diff --git a/java/com/android/incallui/video/bindings/VideoBindings.java b/java/com/android/incallui/video/bindings/VideoBindings.java
new file mode 100644
index 000000000..934ff078a
--- /dev/null
+++ b/java/com/android/incallui/video/bindings/VideoBindings.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.video.bindings;
+
+import com.android.incallui.video.impl.VideoCallFragment;
+import com.android.incallui.video.protocol.VideoCallScreen;
+
+/** Bindings for video module. */
+public class VideoBindings {
+
+ public static VideoCallScreen createVideoCallScreen() {
+ return new VideoCallFragment();
+ }
+}
diff --git a/java/com/android/incallui/video/impl/AndroidManifest.xml b/java/com/android/incallui/video/impl/AndroidManifest.xml
new file mode 100644
index 000000000..a36828e29
--- /dev/null
+++ b/java/com/android/incallui/video/impl/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.incallui.video.impl">
+</manifest>
diff --git a/java/com/android/incallui/video/impl/CameraPermissionDialogFragment.java b/java/com/android/incallui/video/impl/CameraPermissionDialogFragment.java
new file mode 100644
index 000000000..291fce4a0
--- /dev/null
+++ b/java/com/android/incallui/video/impl/CameraPermissionDialogFragment.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.video.impl;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import com.android.dialer.common.FragmentUtils;
+
+/** Dialog fragment to ask for camera permission from user. */
+public class CameraPermissionDialogFragment extends DialogFragment {
+
+ static CameraPermissionDialogFragment newInstance() {
+ CameraPermissionDialogFragment fragment = new CameraPermissionDialogFragment();
+ return fragment;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle bundle) {
+ return new AlertDialog.Builder(getContext())
+ .setTitle(R.string.camera_permission_dialog_title)
+ .setMessage(R.string.camera_permission_dialog_message)
+ .setPositiveButton(
+ R.string.camera_permission_dialog_positive_button,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ VideoCallFragment fragment =
+ FragmentUtils.getParentUnsafe(
+ CameraPermissionDialogFragment.this, VideoCallFragment.class);
+ fragment.onCameraPermissionGranted();
+ }
+ })
+ .setNegativeButton(
+ R.string.camera_permission_dialog_negative_button,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ })
+ .create();
+ }
+}
diff --git a/java/com/android/incallui/video/impl/CheckableImageButton.java b/java/com/android/incallui/video/impl/CheckableImageButton.java
new file mode 100644
index 000000000..320f0571a
--- /dev/null
+++ b/java/com/android/incallui/video/impl/CheckableImageButton.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.video.impl;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.SoundEffectConstants;
+import android.widget.Checkable;
+import android.widget.ImageButton;
+
+/** Image button that maintains a checked state. */
+public class CheckableImageButton extends ImageButton implements Checkable {
+
+ private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked};
+
+ /** Callback interface to notify when the button's checked state has changed */
+ public interface OnCheckedChangeListener {
+
+ void onCheckedChanged(CheckableImageButton button, boolean isChecked);
+ }
+
+ private boolean broadcasting;
+ private boolean isChecked;
+ private OnCheckedChangeListener onCheckedChangeListener;
+ private CharSequence contentDescriptionChecked;
+ private CharSequence contentDescriptionUnchecked;
+
+ public CheckableImageButton(Context context) {
+ this(context, null);
+ }
+
+ public CheckableImageButton(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CheckableImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context, attrs);
+ }
+
+ private void init(Context context, AttributeSet attrs) {
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CheckableImageButton);
+ setChecked(typedArray.getBoolean(R.styleable.CheckableImageButton_android_checked, false));
+ contentDescriptionChecked =
+ typedArray.getText(R.styleable.CheckableImageButton_contentDescriptionChecked);
+ contentDescriptionUnchecked =
+ typedArray.getText(R.styleable.CheckableImageButton_contentDescriptionUnchecked);
+ typedArray.recycle();
+
+ updateContentDescription();
+ setClickable(true);
+ setFocusable(true);
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ performSetChecked(checked);
+ }
+
+ /**
+ * Called when the state of the button should be updated, this should not be the result of user
+ * interaction.
+ *
+ * @param checked {@code true} if the button should be in the checked state, {@code false}
+ * otherwise.
+ */
+ private void performSetChecked(boolean checked) {
+ if (isChecked() == checked) {
+ return;
+ }
+ isChecked = checked;
+ CharSequence contentDescription = updateContentDescription();
+ announceForAccessibility(contentDescription);
+ refreshDrawableState();
+ }
+
+ private CharSequence updateContentDescription() {
+ CharSequence contentDescription =
+ isChecked ? contentDescriptionChecked : contentDescriptionUnchecked;
+ setContentDescription(contentDescription);
+ return contentDescription;
+ }
+
+ /**
+ * Called when the user interacts with a button. This should not result in the button updating
+ * state, rather the request should be propagated to the associated listener.
+ *
+ * @param checked {@code true} if the button should be in the checked state, {@code false}
+ * otherwise.
+ */
+ private void userRequestedSetChecked(boolean checked) {
+ if (isChecked() == checked) {
+ return;
+ }
+ if (broadcasting) {
+ return;
+ }
+ broadcasting = true;
+ if (onCheckedChangeListener != null) {
+ onCheckedChangeListener.onCheckedChanged(this, checked);
+ }
+ broadcasting = false;
+ }
+
+ @Override
+ public boolean isChecked() {
+ return isChecked;
+ }
+
+ @Override
+ public void toggle() {
+ userRequestedSetChecked(!isChecked());
+ }
+
+ @Override
+ public int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+ if (isChecked()) {
+ mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+ }
+ return drawableState;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ invalidate();
+ }
+
+ public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
+ this.onCheckedChangeListener = listener;
+ }
+
+ @Override
+ public boolean performClick() {
+ if (!isCheckable()) {
+ return super.performClick();
+ }
+
+ toggle();
+ final boolean handled = super.performClick();
+ if (!handled) {
+ // View only makes a sound effect if the onClickListener was
+ // called, so we'll need to make one here instead.
+ playSoundEffect(SoundEffectConstants.CLICK);
+ }
+ return handled;
+ }
+
+ private boolean isCheckable() {
+ return onCheckedChangeListener != null;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ SavedState savedState = (SavedState) state;
+ super.onRestoreInstanceState(savedState.getSuperState());
+ performSetChecked(savedState.isChecked);
+ requestLayout();
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ return new SavedState(isChecked(), super.onSaveInstanceState());
+ }
+
+ private static class SavedState extends BaseSavedState {
+
+ public final boolean isChecked;
+
+ private SavedState(boolean isChecked, Parcelable superState) {
+ super(superState);
+ this.isChecked = isChecked;
+ }
+
+ protected SavedState(Parcel in) {
+ super(in);
+ isChecked = in.readByte() != 0;
+ }
+
+ public static final Creator<SavedState> CREATOR =
+ new Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeByte((byte) (isChecked ? 1 : 0));
+ }
+ }
+}
diff --git a/java/com/android/incallui/video/impl/SpeakerButtonController.java b/java/com/android/incallui/video/impl/SpeakerButtonController.java
new file mode 100644
index 000000000..e12032abf
--- /dev/null
+++ b/java/com/android/incallui/video/impl/SpeakerButtonController.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.video.impl;
+
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.StringRes;
+import android.telecom.CallAudioState;
+import android.view.View;
+import android.view.View.OnClickListener;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
+import com.android.incallui.video.impl.CheckableImageButton.OnCheckedChangeListener;
+import com.android.incallui.video.protocol.VideoCallScreenDelegate;
+
+/** Manages a single button. */
+public class SpeakerButtonController implements OnCheckedChangeListener, OnClickListener {
+
+ @NonNull private final InCallButtonUiDelegate inCallButtonUiDelegate;
+ @NonNull private final VideoCallScreenDelegate videoCallScreenDelegate;
+
+ @NonNull private CheckableImageButton button;
+
+ @DrawableRes private int icon = R.drawable.quantum_ic_volume_up_white_36;
+
+ private boolean isChecked;
+ private boolean checkable;
+ private boolean isEnabled;
+ private CharSequence contentDescription;
+
+ public SpeakerButtonController(
+ @NonNull CheckableImageButton button,
+ @NonNull InCallButtonUiDelegate inCallButtonUiDelegate,
+ @NonNull VideoCallScreenDelegate videoCallScreenDelegate) {
+ this.inCallButtonUiDelegate = Assert.isNotNull(inCallButtonUiDelegate);
+ this.videoCallScreenDelegate = Assert.isNotNull(videoCallScreenDelegate);
+ this.button = Assert.isNotNull(button);
+ }
+
+ public void setEnabled(boolean isEnabled) {
+ this.isEnabled = isEnabled;
+ }
+
+ public void updateButtonState() {
+ button.setVisibility(View.VISIBLE);
+ button.setEnabled(isEnabled);
+ button.setChecked(isChecked);
+ button.setOnClickListener(checkable ? null : this);
+ button.setOnCheckedChangeListener(checkable ? this : null);
+ button.setImageResource(icon);
+ button.setContentDescription(contentDescription);
+ }
+
+ public void setAudioState(CallAudioState audioState) {
+ LogUtil.i("SpeakerButtonController.setSupportedAudio", "audioState: " + audioState);
+
+ @StringRes int contentDescriptionResId;
+ if ((audioState.getSupportedRouteMask() & CallAudioState.ROUTE_BLUETOOTH)
+ == CallAudioState.ROUTE_BLUETOOTH) {
+ checkable = false;
+ isChecked = false;
+
+ if ((audioState.getRoute() & CallAudioState.ROUTE_BLUETOOTH)
+ == CallAudioState.ROUTE_BLUETOOTH) {
+ icon = R.drawable.quantum_ic_bluetooth_audio_white_36;
+ contentDescriptionResId = R.string.incall_content_description_bluetooth;
+ } else if ((audioState.getRoute() & CallAudioState.ROUTE_SPEAKER)
+ == CallAudioState.ROUTE_SPEAKER) {
+ icon = R.drawable.quantum_ic_volume_up_white_36;
+ contentDescriptionResId = R.string.incall_content_description_speaker;
+ } else if ((audioState.getRoute() & CallAudioState.ROUTE_WIRED_HEADSET)
+ == CallAudioState.ROUTE_WIRED_HEADSET) {
+ icon = R.drawable.quantum_ic_headset_white_36;
+ contentDescriptionResId = R.string.incall_content_description_headset;
+ } else {
+ icon = R.drawable.ic_phone_audio_white_36dp;
+ contentDescriptionResId = R.string.incall_content_description_earpiece;
+ }
+ } else {
+ checkable = true;
+ isChecked = audioState.getRoute() == CallAudioState.ROUTE_SPEAKER;
+ icon = R.drawable.quantum_ic_volume_up_white_36;
+ contentDescriptionResId = R.string.incall_content_description_speaker;
+ }
+
+ contentDescription = button.getContext().getText(contentDescriptionResId);
+ updateButtonState();
+ }
+
+ @Override
+ public void onCheckedChanged(CheckableImageButton button, boolean isChecked) {
+ LogUtil.i("SpeakerButtonController.onCheckedChanged", null);
+ inCallButtonUiDelegate.toggleSpeakerphone();
+ videoCallScreenDelegate.resetAutoFullscreenTimer();
+ }
+
+ @Override
+ public void onClick(View view) {
+ LogUtil.i("SpeakerButtonController.onClick", null);
+ inCallButtonUiDelegate.showAudioRouteSelector();
+ videoCallScreenDelegate.resetAutoFullscreenTimer();
+ }
+}
diff --git a/java/com/android/incallui/video/impl/SwitchOnHoldCallController.java b/java/com/android/incallui/video/impl/SwitchOnHoldCallController.java
new file mode 100644
index 000000000..372b56b4e
--- /dev/null
+++ b/java/com/android/incallui/video/impl/SwitchOnHoldCallController.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.video.impl;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.View;
+import android.view.View.OnClickListener;
+import com.android.dialer.common.Assert;
+import com.android.incallui.incall.protocol.InCallScreenDelegate;
+import com.android.incallui.incall.protocol.SecondaryInfo;
+import com.android.incallui.video.protocol.VideoCallScreenDelegate;
+
+/** Manages the swap button and on hold banner. */
+public class SwitchOnHoldCallController implements OnClickListener {
+
+ @NonNull private InCallScreenDelegate inCallScreenDelegate;
+ @NonNull private VideoCallScreenDelegate videoCallScreenDelegate;
+
+ @NonNull private View switchOnHoldButton;
+
+ @NonNull private View onHoldBanner;
+
+ private boolean isVisible;
+
+ private boolean isEnabled;
+
+ @Nullable private SecondaryInfo secondaryInfo;
+
+ public SwitchOnHoldCallController(
+ @NonNull View switchOnHoldButton,
+ @NonNull View onHoldBanner,
+ @NonNull InCallScreenDelegate inCallScreenDelegate,
+ @NonNull VideoCallScreenDelegate videoCallScreenDelegate) {
+ this.switchOnHoldButton = Assert.isNotNull(switchOnHoldButton);
+ switchOnHoldButton.setOnClickListener(this);
+ this.onHoldBanner = Assert.isNotNull(onHoldBanner);
+ this.inCallScreenDelegate = Assert.isNotNull(inCallScreenDelegate);
+ this.videoCallScreenDelegate = Assert.isNotNull(videoCallScreenDelegate);
+ }
+
+ public void setEnabled(boolean isEnabled) {
+ this.isEnabled = isEnabled;
+ updateButtonState();
+ }
+
+ public void setVisible(boolean isVisible) {
+ this.isVisible = isVisible;
+ updateButtonState();
+ }
+
+ public void setOnScreen() {
+ isVisible = hasSecondaryInfo();
+ updateButtonState();
+ }
+
+ public void setSecondaryInfo(@Nullable SecondaryInfo secondaryInfo) {
+ this.secondaryInfo = secondaryInfo;
+ isVisible = hasSecondaryInfo();
+ }
+
+ private boolean hasSecondaryInfo() {
+ return secondaryInfo != null && secondaryInfo.shouldShow;
+ }
+
+ public void updateButtonState() {
+ switchOnHoldButton.setEnabled(isEnabled);
+ switchOnHoldButton.setVisibility(isVisible ? View.VISIBLE : View.INVISIBLE);
+ onHoldBanner.setVisibility(isVisible ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ @Override
+ public void onClick(View view) {
+ inCallScreenDelegate.onSecondaryInfoClicked();
+ videoCallScreenDelegate.resetAutoFullscreenTimer();
+ }
+}
diff --git a/java/com/android/incallui/video/impl/VideoCallFragment.java b/java/com/android/incallui/video/impl/VideoCallFragment.java
new file mode 100644
index 000000000..77a67d032
--- /dev/null
+++ b/java/com/android/incallui/video/impl/VideoCallFragment.java
@@ -0,0 +1,1215 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.video.impl;
+
+import android.Manifest.permission;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Outline;
+import android.graphics.Point;
+import android.graphics.drawable.Animatable;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.renderscript.Allocation;
+import android.renderscript.Element;
+import android.renderscript.RenderScript;
+import android.renderscript.ScriptIntrinsicBlur;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.view.animation.FastOutLinearInInterpolator;
+import android.support.v4.view.animation.LinearOutSlowInInterpolator;
+import android.telecom.CallAudioState;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnSystemUiVisibilityChangeListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.view.ViewOutlineProvider;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.FragmentUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.ActivityCompat;
+import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment;
+import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRouteSelectorPresenter;
+import com.android.incallui.call.VideoUtils;
+import com.android.incallui.contactgrid.ContactGridManager;
+import com.android.incallui.hold.OnHoldFragment;
+import com.android.incallui.incall.protocol.InCallButtonIds;
+import com.android.incallui.incall.protocol.InCallButtonIdsExtension;
+import com.android.incallui.incall.protocol.InCallButtonUi;
+import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
+import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory;
+import com.android.incallui.incall.protocol.InCallScreen;
+import com.android.incallui.incall.protocol.InCallScreenDelegate;
+import com.android.incallui.incall.protocol.InCallScreenDelegateFactory;
+import com.android.incallui.incall.protocol.PrimaryCallState;
+import com.android.incallui.incall.protocol.PrimaryInfo;
+import com.android.incallui.incall.protocol.SecondaryInfo;
+import com.android.incallui.video.impl.CheckableImageButton.OnCheckedChangeListener;
+import com.android.incallui.video.protocol.VideoCallScreen;
+import com.android.incallui.video.protocol.VideoCallScreenDelegate;
+import com.android.incallui.video.protocol.VideoCallScreenDelegateFactory;
+import com.android.incallui.videosurface.bindings.VideoSurfaceBindings;
+import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
+
+/** Contains UI elements for a video call. */
+public class VideoCallFragment extends Fragment
+ implements InCallScreen,
+ InCallButtonUi,
+ VideoCallScreen,
+ OnClickListener,
+ OnCheckedChangeListener,
+ AudioRouteSelectorPresenter,
+ OnSystemUiVisibilityChangeListener {
+
+ private static final float BLUR_PREVIEW_RADIUS = 16.0f;
+ private static final float BLUR_PREVIEW_SCALE_FACTOR = 1.0f;
+ private static final float BLUR_REMOTE_RADIUS = 25.0f;
+ private static final float BLUR_REMOTE_SCALE_FACTOR = 0.25f;
+ private static final float ASPECT_RATIO_MATCH_THRESHOLD = 0.2f;
+
+ private static final int CAMERA_PERMISSION_REQUEST_CODE = 1;
+ private static final String CAMERA_PERMISSION_DIALOG_FRAMENT_TAG =
+ "CameraPermissionDialogFragment";
+ private static final long CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS = 2000L;
+ private static final long VIDEO_OFF_VIEW_FADE_OUT_DELAY_IN_MILLIS = 2000L;
+
+ private final ViewOutlineProvider circleOutlineProvider =
+ new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ int x = view.getWidth() / 2;
+ int y = view.getHeight() / 2;
+ int radius = Math.min(x, y);
+ outline.setOval(x - radius, y - radius, x + radius, y + radius);
+ }
+ };
+ private InCallScreenDelegate inCallScreenDelegate;
+ private VideoCallScreenDelegate videoCallScreenDelegate;
+ private InCallButtonUiDelegate inCallButtonUiDelegate;
+ private View endCallButton;
+ private CheckableImageButton speakerButton;
+ private SpeakerButtonController speakerButtonController;
+ private CheckableImageButton muteButton;
+ private CheckableImageButton cameraOffButton;
+ private ImageButton swapCameraButton;
+ private View switchOnHoldButton;
+ private View onHoldContainer;
+ private SwitchOnHoldCallController switchOnHoldCallController;
+ private TextView remoteVideoOff;
+ private ImageView remoteOffBlurredImageView;
+ private View mutePreviewOverlay;
+ private View previewOffOverlay;
+ private ImageView previewOffBlurredImageView;
+ private View controls;
+ private View controlsContainer;
+ private TextureView previewTextureView;
+ private TextureView remoteTextureView;
+ private View greenScreenBackgroundView;
+ private View fullscreenBackgroundView;
+ private boolean shouldShowRemote;
+ private boolean shouldShowPreview;
+ private boolean isInFullscreenMode;
+ private boolean isInGreenScreenMode;
+ private boolean hasInitializedScreenModes;
+ private boolean isRemotelyHeld;
+ private ContactGridManager contactGridManager;
+ private SecondaryInfo savedSecondaryInfo;
+ private final Runnable cameraPermissionDialogRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ if (videoCallScreenDelegate.shouldShowCameraPermissionDialog()) {
+ LogUtil.i("VideoCallFragment.cameraPermissionDialogRunnable", "showing dialog");
+ checkCameraPermission();
+ }
+ }
+ };
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ LogUtil.i("VideoCallFragment.onCreate", null);
+
+ inCallButtonUiDelegate =
+ FragmentUtils.getParent(this, InCallButtonUiDelegateFactory.class)
+ .newInCallButtonUiDelegate();
+ if (savedInstanceState != null) {
+ inCallButtonUiDelegate.onRestoreInstanceState(savedInstanceState);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ LogUtil.i("VideoCallFragment.onRequestPermissionsResult", "Camera permission granted.");
+ videoCallScreenDelegate.onCameraPermissionGranted();
+ } else {
+ LogUtil.i("VideoCallFragment.onRequestPermissionsResult", "Camera permission denied.");
+ }
+ }
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+ LogUtil.i("VideoCallFragment.onCreateView", null);
+
+ View view =
+ layoutInflater.inflate(
+ isLandscape() ? R.layout.frag_videocall_land : R.layout.frag_videocall,
+ viewGroup,
+ false);
+ contactGridManager =
+ new ContactGridManager(view, null /* no avatar */, 0, false /* showAnonymousAvatar */);
+
+ controls = view.findViewById(R.id.videocall_video_controls);
+ controls.setVisibility(
+ ActivityCompat.isInMultiWindowMode(getActivity()) ? View.GONE : View.VISIBLE);
+ controlsContainer = view.findViewById(R.id.videocall_video_controls_container);
+ speakerButton = (CheckableImageButton) view.findViewById(R.id.videocall_speaker_button);
+ muteButton = (CheckableImageButton) view.findViewById(R.id.videocall_mute_button);
+ muteButton.setOnCheckedChangeListener(this);
+ mutePreviewOverlay = view.findViewById(R.id.videocall_video_preview_mute_overlay);
+ cameraOffButton = (CheckableImageButton) view.findViewById(R.id.videocall_mute_video);
+ cameraOffButton.setOnCheckedChangeListener(this);
+ previewOffOverlay = view.findViewById(R.id.videocall_video_preview_off_overlay);
+ previewOffBlurredImageView =
+ (ImageView) view.findViewById(R.id.videocall_preview_off_blurred_image_view);
+ swapCameraButton = (ImageButton) view.findViewById(R.id.videocall_switch_video);
+ swapCameraButton.setOnClickListener(this);
+ view.findViewById(R.id.videocall_switch_controls)
+ .setVisibility(
+ ActivityCompat.isInMultiWindowMode(getActivity()) ? View.GONE : View.VISIBLE);
+ switchOnHoldButton = view.findViewById(R.id.videocall_switch_on_hold);
+ onHoldContainer = view.findViewById(R.id.videocall_on_hold_banner);
+ remoteVideoOff = (TextView) view.findViewById(R.id.videocall_remote_video_off);
+ remoteVideoOff.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
+ remoteOffBlurredImageView =
+ (ImageView) view.findViewById(R.id.videocall_remote_off_blurred_image_view);
+ endCallButton = view.findViewById(R.id.videocall_end_call);
+ endCallButton.setOnClickListener(this);
+ previewTextureView = (TextureView) view.findViewById(R.id.videocall_video_preview);
+ previewTextureView.setClipToOutline(true);
+ previewOffOverlay.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ checkCameraPermission();
+ }
+ });
+ remoteTextureView = (TextureView) view.findViewById(R.id.videocall_video_remote);
+ greenScreenBackgroundView = view.findViewById(R.id.videocall_green_screen_background);
+ fullscreenBackgroundView = view.findViewById(R.id.videocall_fullscreen_background);
+
+ // We need the texture view size to be able to scale the remote video. At this point the view
+ // layout won't be complete so add a layout listener.
+ ViewTreeObserver observer = remoteTextureView.getViewTreeObserver();
+ observer.addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ LogUtil.i("VideoCallFragment.onGlobalLayout", null);
+ updateRemoteVideoScaling();
+ updatePreviewVideoScaling();
+ updateVideoOffViews();
+ // Remove the listener so we don't continually re-layout.
+ ViewTreeObserver observer = remoteTextureView.getViewTreeObserver();
+ if (observer.isAlive()) {
+ observer.removeOnGlobalLayoutListener(this);
+ }
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle bundle) {
+ super.onViewCreated(view, bundle);
+ LogUtil.i("VideoCallFragment.onViewCreated", null);
+
+ inCallScreenDelegate =
+ FragmentUtils.getParentUnsafe(this, InCallScreenDelegateFactory.class)
+ .newInCallScreenDelegate();
+ videoCallScreenDelegate =
+ FragmentUtils.getParentUnsafe(this, VideoCallScreenDelegateFactory.class)
+ .newVideoCallScreenDelegate();
+
+ speakerButtonController =
+ new SpeakerButtonController(speakerButton, inCallButtonUiDelegate, videoCallScreenDelegate);
+ switchOnHoldCallController =
+ new SwitchOnHoldCallController(
+ switchOnHoldButton, onHoldContainer, inCallScreenDelegate, videoCallScreenDelegate);
+
+ videoCallScreenDelegate.initVideoCallScreenDelegate(getContext(), this);
+
+ inCallScreenDelegate.onInCallScreenDelegateInit(this);
+ inCallScreenDelegate.onInCallScreenReady();
+ inCallButtonUiDelegate.onInCallButtonUiReady(this);
+
+ view.setOnSystemUiVisibilityChangeListener(this);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ inCallButtonUiDelegate.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ LogUtil.i("VideoCallFragment.onDestroyView", null);
+ inCallButtonUiDelegate.onInCallButtonUiUnready();
+ inCallScreenDelegate.onInCallScreenUnready();
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (savedSecondaryInfo != null) {
+ setSecondary(savedSecondaryInfo);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ LogUtil.i("VideoCallFragment.onResume", null);
+ inCallScreenDelegate.onInCallScreenResumed();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ LogUtil.i("VideoCallFragment.onStart", null);
+ inCallButtonUiDelegate.refreshMuteState();
+ videoCallScreenDelegate.onVideoCallScreenUiReady();
+ getView().postDelayed(cameraPermissionDialogRunnable, CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ LogUtil.i("VideoCallFragment.onPause", null);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ LogUtil.i("VideoCallFragment.onStop", null);
+ getView().removeCallbacks(cameraPermissionDialogRunnable);
+ videoCallScreenDelegate.onVideoCallScreenUiUnready();
+ }
+
+ private void exitFullscreenMode() {
+ LogUtil.i("VideoCallFragment.exitFullscreenMode", null);
+
+ if (!getView().isAttachedToWindow()) {
+ LogUtil.i("VideoCallFragment.exitFullscreenMode", "not attached");
+ return;
+ }
+
+ showSystemUI();
+
+ LinearOutSlowInInterpolator linearOutSlowInInterpolator = new LinearOutSlowInInterpolator();
+
+ // Animate the controls to the shown state.
+ controls
+ .animate()
+ .translationX(0)
+ .translationY(0)
+ .setInterpolator(linearOutSlowInInterpolator)
+ .alpha(1)
+ .start();
+
+ // Animate onHold to the shown state.
+ switchOnHoldButton
+ .animate()
+ .translationX(0)
+ .translationY(0)
+ .setInterpolator(linearOutSlowInInterpolator)
+ .alpha(1)
+ .withStartAction(
+ new Runnable() {
+ @Override
+ public void run() {
+ switchOnHoldCallController.setOnScreen();
+ }
+ });
+
+ View contactGridView = contactGridManager.getContainerView();
+ // Animate contact grid to the shown state.
+ contactGridView
+ .animate()
+ .translationX(0)
+ .translationY(0)
+ .setInterpolator(linearOutSlowInInterpolator)
+ .alpha(1)
+ .withStartAction(
+ new Runnable() {
+ @Override
+ public void run() {
+ contactGridManager.show();
+ }
+ });
+
+ endCallButton
+ .animate()
+ .translationX(0)
+ .translationY(0)
+ .setInterpolator(linearOutSlowInInterpolator)
+ .alpha(1)
+ .withStartAction(
+ new Runnable() {
+ @Override
+ public void run() {
+ endCallButton.setVisibility(View.VISIBLE);
+ }
+ })
+ .start();
+
+ // Animate all the preview controls up to make room for the navigation bar.
+ // In green screen mode we don't need this because the preview takes up the whole screen and has
+ // a fixed position.
+ if (!isInGreenScreenMode) {
+ Point previewOffsetStartShown = getPreviewOffsetStartShown();
+ for (View view : getAllPreviewRelatedViews()) {
+ // Animate up with the preview offset above the navigation bar.
+ view.animate()
+ .translationX(previewOffsetStartShown.x)
+ .translationY(previewOffsetStartShown.y)
+ .setInterpolator(new AccelerateDecelerateInterpolator())
+ .start();
+ }
+ }
+
+ updateOverlayBackground();
+ }
+
+ private void showSystemUI() {
+ View view = getView();
+ if (view != null) {
+ // Code is more expressive with all flags present, even though some may be combined
+ //noinspection PointlessBitwiseExpression
+ view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ }
+ }
+
+ /** Set view flags to hide the system UI. System UI will return on any touch event */
+ private void hideSystemUI() {
+ View view = getView();
+ if (view != null) {
+ view.setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ }
+ }
+
+ private Point getControlsOffsetEndHidden(View controls) {
+ if (isLandscape()) {
+ return new Point(0, getOffsetBottom(controls));
+ } else {
+ return new Point(getOffsetStart(controls), 0);
+ }
+ }
+
+ private Point getSwitchOnHoldOffsetEndHidden(View swapCallButton) {
+ if (isLandscape()) {
+ return new Point(0, getOffsetTop(swapCallButton));
+ } else {
+ return new Point(getOffsetEnd(swapCallButton), 0);
+ }
+ }
+
+ private Point getContactGridOffsetEndHidden(View view) {
+ return new Point(0, getOffsetTop(view));
+ }
+
+ private Point getEndCallOffsetEndHidden(View endCallButton) {
+ if (isLandscape()) {
+ return new Point(getOffsetEnd(endCallButton), 0);
+ } else {
+ return new Point(0, ((MarginLayoutParams) endCallButton.getLayoutParams()).bottomMargin);
+ }
+ }
+
+ private Point getPreviewOffsetStartShown() {
+ // No insets in multiwindow mode, and rootWindowInsets will get the display's insets.
+ if (ActivityCompat.isInMultiWindowMode(getActivity())) {
+ return new Point();
+ }
+ if (isLandscape()) {
+ int stableInsetEnd =
+ getView().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
+ ? getView().getRootWindowInsets().getStableInsetLeft()
+ : -getView().getRootWindowInsets().getStableInsetRight();
+ return new Point(stableInsetEnd, 0);
+ } else {
+ return new Point(0, -getView().getRootWindowInsets().getStableInsetBottom());
+ }
+ }
+
+ private View[] getAllPreviewRelatedViews() {
+ return new View[] {
+ previewTextureView, previewOffOverlay, previewOffBlurredImageView, mutePreviewOverlay,
+ };
+ }
+
+ private int getOffsetTop(View view) {
+ return -(view.getHeight() + ((MarginLayoutParams) view.getLayoutParams()).topMargin);
+ }
+
+ private int getOffsetBottom(View view) {
+ return view.getHeight() + ((MarginLayoutParams) view.getLayoutParams()).bottomMargin;
+ }
+
+ private int getOffsetStart(View view) {
+ int offset = view.getWidth() + ((MarginLayoutParams) view.getLayoutParams()).getMarginStart();
+ if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+ offset = -offset;
+ }
+ return -offset;
+ }
+
+ private int getOffsetEnd(View view) {
+ int offset = view.getWidth() + ((MarginLayoutParams) view.getLayoutParams()).getMarginEnd();
+ if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+ offset = -offset;
+ }
+ return offset;
+ }
+
+ private void enterFullscreenMode() {
+ LogUtil.i("VideoCallFragment.enterFullscreenMode", null);
+
+ hideSystemUI();
+
+ Interpolator fastOutLinearInInterpolator = new FastOutLinearInInterpolator();
+
+ // Animate controls to the hidden state.
+ Point offset = getControlsOffsetEndHidden(controls);
+ controls
+ .animate()
+ .translationX(offset.x)
+ .translationY(offset.y)
+ .setInterpolator(fastOutLinearInInterpolator)
+ .alpha(0)
+ .start();
+
+ // Animate onHold to the hidden state.
+ offset = getSwitchOnHoldOffsetEndHidden(switchOnHoldButton);
+ switchOnHoldButton
+ .animate()
+ .translationX(offset.x)
+ .translationY(offset.y)
+ .setInterpolator(fastOutLinearInInterpolator)
+ .alpha(0);
+
+ View contactGridView = contactGridManager.getContainerView();
+ // Animate contact grid to the hidden state.
+ offset = getContactGridOffsetEndHidden(contactGridView);
+ contactGridView
+ .animate()
+ .translationX(offset.x)
+ .translationY(offset.y)
+ .setInterpolator(fastOutLinearInInterpolator)
+ .alpha(0);
+
+ offset = getEndCallOffsetEndHidden(endCallButton);
+ // Use a fast out interpolator to quickly fade out the button. This is important because the
+ // button can't draw under the navigation bar which means that it'll look weird if it just
+ // abruptly disappears when it reaches the edge of the naivgation bar.
+ endCallButton
+ .animate()
+ .translationX(offset.x)
+ .translationY(offset.y)
+ .setInterpolator(fastOutLinearInInterpolator)
+ .alpha(0)
+ .withEndAction(
+ new Runnable() {
+ @Override
+ public void run() {
+ endCallButton.setVisibility(View.INVISIBLE);
+ }
+ })
+ .setInterpolator(new FastOutLinearInInterpolator())
+ .start();
+
+ // Animate all the preview controls down now that the navigation bar is hidden.
+ // In green screen mode we don't need this because the preview takes up the whole screen and has
+ // a fixed position.
+ if (!isInGreenScreenMode) {
+ for (View view : getAllPreviewRelatedViews()) {
+ // Animate down with the navigation bar hidden.
+ view.animate()
+ .translationX(0)
+ .translationY(0)
+ .setInterpolator(new AccelerateDecelerateInterpolator())
+ .start();
+ }
+ }
+ updateOverlayBackground();
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == endCallButton) {
+ LogUtil.i("VideoCallFragment.onClick", "end call button clicked");
+ inCallButtonUiDelegate.onEndCallClicked();
+ videoCallScreenDelegate.resetAutoFullscreenTimer();
+ } else if (v == swapCameraButton) {
+ if (swapCameraButton.getDrawable() instanceof Animatable) {
+ ((Animatable) swapCameraButton.getDrawable()).start();
+ }
+ inCallButtonUiDelegate.toggleCameraClicked();
+ videoCallScreenDelegate.resetAutoFullscreenTimer();
+ }
+ }
+
+ @Override
+ public void onCheckedChanged(CheckableImageButton button, boolean isChecked) {
+ if (button == cameraOffButton) {
+ if (!isChecked && !VideoUtils.hasCameraPermissionAndAllowedByUser(getContext())) {
+ LogUtil.i("VideoCallFragment.onCheckedChanged", "show camera permission dialog");
+ checkCameraPermission();
+ } else {
+ inCallButtonUiDelegate.pauseVideoClicked(isChecked);
+ videoCallScreenDelegate.resetAutoFullscreenTimer();
+ }
+ } else if (button == muteButton) {
+ inCallButtonUiDelegate.muteClicked(isChecked);
+ videoCallScreenDelegate.resetAutoFullscreenTimer();
+ }
+ }
+
+ @Override
+ public void showVideoViews(
+ boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld) {
+ LogUtil.i(
+ "VideoCallFragment.showVideoViews",
+ "showPreview: %b, shouldShowRemote: %b",
+ shouldShowPreview,
+ shouldShowRemote);
+ this.shouldShowPreview = shouldShowPreview;
+ this.shouldShowRemote = shouldShowRemote;
+ this.isRemotelyHeld = isRemotelyHeld;
+
+ videoCallScreenDelegate.getLocalVideoSurfaceTexture().attachToTextureView(previewTextureView);
+ videoCallScreenDelegate.getRemoteVideoSurfaceTexture().attachToTextureView(remoteTextureView);
+
+ updateVideoOffViews();
+ updateRemoteVideoScaling();
+ }
+
+ /**
+ * This method scales the video feed inside the texture view, it doesn't change the texture view's
+ * size. In the old UI we would change the view size to match the aspect ratio of the video. In
+ * the new UI the view is always square (with the circular clip) so we have to do additional work
+ * to make sure the non-square video doesn't look squished.
+ */
+ @Override
+ public void onLocalVideoDimensionsChanged() {
+ LogUtil.i("VideoCallFragment.onLocalVideoDimensionsChanged", null);
+ updatePreviewVideoScaling();
+ }
+
+ @Override
+ public void onLocalVideoOrientationChanged() {
+ LogUtil.i("VideoCallFragment.onLocalVideoOrientationChanged", null);
+ updatePreviewVideoScaling();
+ }
+
+ /** Called when the remote video's dimensions change. */
+ @Override
+ public void onRemoteVideoDimensionsChanged() {
+ LogUtil.i("VideoCallFragment.onRemoteVideoDimensionsChanged", null);
+ updateRemoteVideoScaling();
+ }
+
+ @Override
+ public void updateFullscreenAndGreenScreenMode(
+ boolean shouldShowFullscreen, boolean shouldShowGreenScreen) {
+ LogUtil.i(
+ "VideoCallFragment.updateFullscreenAndGreenScreenMode",
+ "shouldShowFullscreen: %b, shouldShowGreenScreen: %b",
+ shouldShowFullscreen,
+ shouldShowGreenScreen);
+
+ if (getActivity() == null) {
+ LogUtil.i("VideoCallFragment.updateFullscreenAndGreenScreenMode", "not attached to activity");
+ return;
+ }
+
+ // Check if anything is actually going to change. The first time this function is called we
+ // force a change by checking the hasInitializedScreenModes flag. We also force both fullscreen
+ // and green screen modes to update even if only one has changed. That's because they both
+ // depend on each other.
+ if (hasInitializedScreenModes
+ && shouldShowGreenScreen == isInGreenScreenMode
+ && shouldShowFullscreen == isInFullscreenMode) {
+ LogUtil.i(
+ "VideoCallFragment.updateFullscreenAndGreenScreenMode", "no change to screen modes");
+ return;
+ }
+ hasInitializedScreenModes = true;
+ isInGreenScreenMode = shouldShowGreenScreen;
+ isInFullscreenMode = shouldShowFullscreen;
+
+ if (getView().isAttachedToWindow() && !ActivityCompat.isInMultiWindowMode(getActivity())) {
+ controlsContainer.onApplyWindowInsets(getView().getRootWindowInsets());
+ }
+ if (shouldShowGreenScreen) {
+ enterGreenScreenMode();
+ } else {
+ exitGreenScreenMode();
+ }
+ if (shouldShowFullscreen) {
+ enterFullscreenMode();
+ } else {
+ exitFullscreenMode();
+ }
+ updateVideoOffViews();
+
+ OnHoldFragment onHoldFragment =
+ ((OnHoldFragment)
+ getChildFragmentManager().findFragmentById(R.id.videocall_on_hold_banner));
+ if (onHoldFragment != null) {
+ onHoldFragment.setPadTopInset(!isInFullscreenMode);
+ }
+ }
+
+ @Override
+ public Fragment getVideoCallScreenFragment() {
+ return this;
+ }
+
+ @Override
+ public void showButton(@InCallButtonIds int buttonId, boolean show) {
+ LogUtil.v(
+ "VideoCallFragment.showButton",
+ "buttonId: %s, show: %b",
+ InCallButtonIdsExtension.toString(buttonId),
+ show);
+ if (buttonId == InCallButtonIds.BUTTON_AUDIO) {
+ speakerButtonController.setEnabled(show);
+ } else if (buttonId == InCallButtonIds.BUTTON_MUTE) {
+ muteButton.setEnabled(show);
+ } else if (buttonId == InCallButtonIds.BUTTON_PAUSE_VIDEO) {
+ cameraOffButton.setEnabled(show);
+ } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) {
+ switchOnHoldCallController.setVisible(show);
+ } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_CAMERA) {
+ swapCameraButton.setEnabled(show);
+ }
+ }
+
+ @Override
+ public void enableButton(@InCallButtonIds int buttonId, boolean enable) {
+ LogUtil.v(
+ "VideoCallFragment.setEnabled",
+ "buttonId: %s, enable: %b",
+ InCallButtonIdsExtension.toString(buttonId),
+ enable);
+ if (buttonId == InCallButtonIds.BUTTON_AUDIO) {
+ speakerButtonController.setEnabled(enable);
+ } else if (buttonId == InCallButtonIds.BUTTON_MUTE) {
+ muteButton.setEnabled(enable);
+ } else if (buttonId == InCallButtonIds.BUTTON_PAUSE_VIDEO) {
+ cameraOffButton.setEnabled(enable);
+ } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) {
+ switchOnHoldCallController.setEnabled(enable);
+ }
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ LogUtil.v("VideoCallFragment.setEnabled", "enabled: " + enabled);
+ speakerButtonController.setEnabled(enabled);
+ muteButton.setEnabled(enabled);
+ cameraOffButton.setEnabled(enabled);
+ switchOnHoldCallController.setEnabled(enabled);
+ }
+
+ @Override
+ public void setHold(boolean value) {
+ LogUtil.i("VideoCallFragment.setHold", "value: " + value);
+ }
+
+ @Override
+ public void setCameraSwitched(boolean isBackFacingCamera) {
+ LogUtil.i("VideoCallFragment.setCameraSwitched", "isBackFacingCamera: " + isBackFacingCamera);
+ }
+
+ @Override
+ public void setVideoPaused(boolean isPaused) {
+ LogUtil.i("VideoCallFragment.setVideoPaused", "isPaused: " + isPaused);
+ cameraOffButton.setChecked(isPaused);
+ }
+
+ @Override
+ public void setAudioState(CallAudioState audioState) {
+ LogUtil.i("VideoCallFragment.setAudioState", "audioState: " + audioState);
+ speakerButtonController.setAudioState(audioState);
+ muteButton.setChecked(audioState.isMuted());
+ updateMutePreviewOverlayVisibility();
+ }
+
+ @Override
+ public void updateButtonStates() {
+ LogUtil.i("VideoCallFragment.updateButtonState", null);
+ speakerButtonController.updateButtonState();
+ switchOnHoldCallController.updateButtonState();
+ }
+
+ @Override
+ public void updateInCallButtonUiColors() {}
+
+ @Override
+ public Fragment getInCallButtonUiFragment() {
+ return this;
+ }
+
+ @Override
+ public void showAudioRouteSelector() {
+ LogUtil.i("VideoCallFragment.showAudioRouteSelector", null);
+ AudioRouteSelectorDialogFragment.newInstance(inCallButtonUiDelegate.getCurrentAudioState())
+ .show(getChildFragmentManager(), null);
+ }
+
+ @Override
+ public void onAudioRouteSelected(int audioRoute) {
+ LogUtil.i("VideoCallFragment.onAudioRouteSelected", "audioRoute: " + audioRoute);
+ inCallButtonUiDelegate.setAudioRoute(audioRoute);
+ }
+
+ @Override
+ public void setPrimary(@NonNull PrimaryInfo primaryInfo) {
+ LogUtil.i("VideoCallFragment.setPrimary", primaryInfo.toString());
+ contactGridManager.setPrimary(primaryInfo);
+ }
+
+ @Override
+ public void setSecondary(@NonNull SecondaryInfo secondaryInfo) {
+ LogUtil.i("VideoCallFragment.setSecondary", secondaryInfo.toString());
+ if (!isAdded()) {
+ savedSecondaryInfo = secondaryInfo;
+ return;
+ }
+ savedSecondaryInfo = null;
+ switchOnHoldCallController.setSecondaryInfo(secondaryInfo);
+ updateButtonStates();
+ FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
+ Fragment oldBanner = getChildFragmentManager().findFragmentById(R.id.videocall_on_hold_banner);
+ if (secondaryInfo.shouldShow) {
+ OnHoldFragment onHoldFragment = OnHoldFragment.newInstance(secondaryInfo);
+ onHoldFragment.setPadTopInset(!isInFullscreenMode);
+ transaction.replace(R.id.videocall_on_hold_banner, onHoldFragment);
+ } else {
+ if (oldBanner != null) {
+ transaction.remove(oldBanner);
+ }
+ }
+ transaction.setCustomAnimations(R.anim.abc_slide_in_top, R.anim.abc_slide_out_top);
+ transaction.commitAllowingStateLoss();
+ }
+
+ @Override
+ public void setCallState(@NonNull PrimaryCallState primaryCallState) {
+ LogUtil.i("VideoCallFragment.setCallState", primaryCallState.toString());
+ contactGridManager.setCallState(primaryCallState);
+ }
+
+ @Override
+ public void setEndCallButtonEnabled(boolean enabled, boolean animate) {
+ LogUtil.i("VideoCallFragment.setEndCallButtonEnabled", "enabled: " + enabled);
+ }
+
+ @Override
+ public void showManageConferenceCallButton(boolean visible) {
+ LogUtil.i("VideoCallFragment.showManageConferenceCallButton", "visible: " + visible);
+ }
+
+ @Override
+ public boolean isManageConferenceVisible() {
+ LogUtil.i("VideoCallFragment.isManageConferenceVisible", null);
+ return false;
+ }
+
+ @Override
+ public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ contactGridManager.dispatchPopulateAccessibilityEvent(event);
+ }
+
+ @Override
+ public void showNoteSentToast() {
+ LogUtil.i("VideoCallFragment.showNoteSentToast", null);
+ }
+
+ @Override
+ public void updateInCallScreenColors() {
+ LogUtil.i("VideoCallFragment.updateColors", null);
+ }
+
+ @Override
+ public void onInCallScreenDialpadVisibilityChange(boolean isShowing) {
+ LogUtil.i("VideoCallFragment.onInCallScreenDialpadVisibilityChange", null);
+ }
+
+ @Override
+ public int getAnswerAndDialpadContainerResourceId() {
+ return 0;
+ }
+
+ @Override
+ public Fragment getInCallScreenFragment() {
+ return this;
+ }
+
+ @Override
+ public boolean isShowingLocationUi() {
+ return false;
+ }
+
+ @Override
+ public void showLocationUi(Fragment locationUi) {
+ LogUtil.e("VideoCallFragment.showLocationUi", "Emergency video calling not supported");
+ // Do nothing
+ }
+
+ private void updatePreviewVideoScaling() {
+ if (previewTextureView.getWidth() == 0 || previewTextureView.getHeight() == 0) {
+ LogUtil.i("VideoCallFragment.updatePreviewVideoScaling", "view layout hasn't finished yet");
+ return;
+ }
+ VideoSurfaceTexture localVideoSurfaceTexture =
+ videoCallScreenDelegate.getLocalVideoSurfaceTexture();
+ Point cameraDimensions = localVideoSurfaceTexture.getSurfaceDimensions();
+ if (cameraDimensions == null) {
+ LogUtil.i(
+ "VideoCallFragment.updatePreviewVideoScaling", "camera dimensions haven't been set");
+ return;
+ }
+ if (isLandscape()) {
+ VideoSurfaceBindings.scaleVideoAndFillView(
+ previewTextureView,
+ cameraDimensions.x,
+ cameraDimensions.y,
+ videoCallScreenDelegate.getDeviceOrientation());
+ } else {
+ VideoSurfaceBindings.scaleVideoAndFillView(
+ previewTextureView,
+ cameraDimensions.y,
+ cameraDimensions.x,
+ videoCallScreenDelegate.getDeviceOrientation());
+ }
+ }
+
+ private void updateRemoteVideoScaling() {
+ VideoSurfaceTexture remoteVideoSurfaceTexture =
+ videoCallScreenDelegate.getRemoteVideoSurfaceTexture();
+ Point videoSize = remoteVideoSurfaceTexture.getSourceVideoDimensions();
+ if (videoSize == null) {
+ LogUtil.i("VideoCallFragment.updateRemoteVideoScaling", "video size is null");
+ return;
+ }
+ if (remoteTextureView.getWidth() == 0 || remoteTextureView.getHeight() == 0) {
+ LogUtil.i("VideoCallFragment.updateRemoteVideoScaling", "view layout hasn't finished yet");
+ return;
+ }
+
+ // If the video and display aspect ratio's are close then scale video to fill display
+ float videoAspectRatio = ((float) videoSize.x) / videoSize.y;
+ float displayAspectRatio =
+ ((float) remoteTextureView.getWidth()) / remoteTextureView.getHeight();
+ float delta = Math.abs(videoAspectRatio - displayAspectRatio);
+ float sum = videoAspectRatio + displayAspectRatio;
+ if (delta / sum < ASPECT_RATIO_MATCH_THRESHOLD) {
+ VideoSurfaceBindings.scaleVideoAndFillView(remoteTextureView, videoSize.x, videoSize.y, 0);
+ } else {
+ VideoSurfaceBindings.scaleVideoMaintainingAspectRatio(
+ remoteTextureView, videoSize.x, videoSize.y);
+ }
+ }
+
+ private boolean isLandscape() {
+ // Choose orientation based on display orientation, not window orientation
+ int rotation = getActivity().getWindowManager().getDefaultDisplay().getRotation();
+ return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270;
+ }
+
+ private void enterGreenScreenMode() {
+ LogUtil.i("VideoCallFragment.enterGreenScreenMode", null);
+ RelativeLayout.LayoutParams params =
+ new RelativeLayout.LayoutParams(
+ RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
+ params.addRule(RelativeLayout.ALIGN_PARENT_START);
+ params.addRule(RelativeLayout.ALIGN_PARENT_TOP);
+ previewTextureView.setLayoutParams(params);
+ previewTextureView.setOutlineProvider(null);
+ updatePreviewVideoScaling();
+ updateOverlayBackground();
+ contactGridManager.setIsMiddleRowVisible(true);
+ updateMutePreviewOverlayVisibility();
+
+ previewOffBlurredImageView.setLayoutParams(params);
+ previewOffBlurredImageView.setOutlineProvider(null);
+ previewOffBlurredImageView.setClipToOutline(false);
+ }
+
+ private void exitGreenScreenMode() {
+ LogUtil.i("VideoCallFragment.exitGreenScreenMode", null);
+ Resources resources = getResources();
+ RelativeLayout.LayoutParams params =
+ new RelativeLayout.LayoutParams(
+ (int) resources.getDimension(R.dimen.videocall_preview_width),
+ (int) resources.getDimension(R.dimen.videocall_preview_height));
+ params.setMargins(
+ 0, 0, 0, (int) resources.getDimension(R.dimen.videocall_preview_margin_bottom));
+ if (isLandscape()) {
+ params.addRule(RelativeLayout.ALIGN_PARENT_END);
+ params.setMarginEnd((int) resources.getDimension(R.dimen.videocall_preview_margin_end));
+ } else {
+ params.addRule(RelativeLayout.ALIGN_PARENT_START);
+ params.setMarginStart((int) resources.getDimension(R.dimen.videocall_preview_margin_start));
+ }
+ params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
+ previewTextureView.setLayoutParams(params);
+ previewTextureView.setOutlineProvider(circleOutlineProvider);
+ updatePreviewVideoScaling();
+ updateOverlayBackground();
+ contactGridManager.setIsMiddleRowVisible(false);
+ updateMutePreviewOverlayVisibility();
+
+ previewOffBlurredImageView.setLayoutParams(params);
+ previewOffBlurredImageView.setOutlineProvider(circleOutlineProvider);
+ previewOffBlurredImageView.setClipToOutline(true);
+ }
+
+ private void updateVideoOffViews() {
+ // Always hide the preview off and remote off views in green screen mode.
+ boolean previewEnabled = isInGreenScreenMode || shouldShowPreview;
+ previewOffOverlay.setVisibility(previewEnabled ? View.GONE : View.VISIBLE);
+ updateBlurredImageView(
+ previewTextureView,
+ previewOffBlurredImageView,
+ shouldShowPreview,
+ BLUR_PREVIEW_RADIUS,
+ BLUR_PREVIEW_SCALE_FACTOR);
+
+ boolean remoteEnabled = isInGreenScreenMode || shouldShowRemote;
+ boolean isResumed = remoteEnabled && !isRemotelyHeld;
+ if (isResumed) {
+ boolean wasRemoteVideoOff =
+ TextUtils.equals(
+ remoteVideoOff.getText(),
+ remoteVideoOff.getResources().getString(R.string.videocall_remote_video_off));
+ // The text needs to be updated and hidden after enough delay in order to be announced by
+ // talkback.
+ remoteVideoOff.setText(
+ wasRemoteVideoOff
+ ? R.string.videocall_remote_video_on
+ : R.string.videocall_remotely_resumed);
+ remoteVideoOff.postDelayed(
+ new Runnable() {
+ @Override
+ public void run() {
+ remoteVideoOff.setVisibility(View.GONE);
+ }
+ },
+ VIDEO_OFF_VIEW_FADE_OUT_DELAY_IN_MILLIS);
+ } else {
+ remoteVideoOff.setText(
+ isRemotelyHeld ? R.string.videocall_remotely_held : R.string.videocall_remote_video_off);
+ remoteVideoOff.setVisibility(View.VISIBLE);
+ }
+ LogUtil.i("VideoCallFragment.updateVideoOffViews", "calling updateBlurredImageView");
+ updateBlurredImageView(
+ remoteTextureView,
+ remoteOffBlurredImageView,
+ shouldShowRemote,
+ BLUR_REMOTE_RADIUS,
+ BLUR_REMOTE_SCALE_FACTOR);
+ }
+
+ private void updateBlurredImageView(
+ TextureView textureView,
+ ImageView blurredImageView,
+ boolean isVideoEnabled,
+ float blurRadius,
+ float scaleFactor) {
+ boolean didBlur = false;
+ long startTimeMillis = SystemClock.elapsedRealtime();
+ if (!isVideoEnabled) {
+ int width = Math.round(textureView.getWidth() * scaleFactor);
+ int height = Math.round(textureView.getHeight() * scaleFactor);
+ // This call takes less than 10 milliseconds.
+ Bitmap bitmap = textureView.getBitmap(width, height);
+ if (bitmap != null) {
+ // TODO: When the view is first displayed after a rotation the bitmap is empty
+ // and thus this blur has no effect.
+ // This call can take 100 milliseconds.
+ blur(getContext(), bitmap, blurRadius);
+
+ // TODO: Figure out why only have to apply the transform in landscape mode
+ if (width > height) {
+ bitmap =
+ Bitmap.createBitmap(
+ bitmap,
+ 0,
+ 0,
+ bitmap.getWidth(),
+ bitmap.getHeight(),
+ textureView.getTransform(null),
+ true);
+ }
+
+ blurredImageView.setImageBitmap(bitmap);
+ blurredImageView.setVisibility(View.VISIBLE);
+ didBlur = true;
+ }
+ }
+ if (!didBlur) {
+ blurredImageView.setImageBitmap(null);
+ blurredImageView.setVisibility(View.GONE);
+ }
+
+ LogUtil.i(
+ "VideoCallFragment.updateBlurredImageView",
+ "didBlur: %b, took %d millis",
+ didBlur,
+ (SystemClock.elapsedRealtime() - startTimeMillis));
+ }
+
+ private void updateOverlayBackground() {
+ if (isInGreenScreenMode) {
+ // We want to darken the preview view to make text and buttons readable. The fullscreen
+ // background is below the preview view so use the green screen background instead.
+ animateSetVisibility(greenScreenBackgroundView, View.VISIBLE);
+ animateSetVisibility(fullscreenBackgroundView, View.GONE);
+ } else if (!isInFullscreenMode) {
+ // We want to darken the remote view to make text and buttons readable. The green screen
+ // background is above the preview view so it would darken the preview too. Use the fullscreen
+ // background instead.
+ animateSetVisibility(greenScreenBackgroundView, View.GONE);
+ animateSetVisibility(fullscreenBackgroundView, View.VISIBLE);
+ } else {
+ animateSetVisibility(greenScreenBackgroundView, View.GONE);
+ animateSetVisibility(fullscreenBackgroundView, View.GONE);
+ }
+ }
+
+ private void updateMutePreviewOverlayVisibility() {
+ // Normally the mute overlay shows on the bottom right of the preview bubble. In green screen
+ // mode the preview is fullscreen so there's no where to anchor it.
+ mutePreviewOverlay.setVisibility(
+ muteButton.isChecked() && !isInGreenScreenMode ? View.VISIBLE : View.GONE);
+ }
+
+ private static void animateSetVisibility(final View view, final int visibility) {
+ if (view.getVisibility() == visibility) {
+ return;
+ }
+
+ int startAlpha;
+ int endAlpha;
+ if (visibility == View.GONE) {
+ startAlpha = 1;
+ endAlpha = 0;
+ } else if (visibility == View.VISIBLE) {
+ startAlpha = 0;
+ endAlpha = 1;
+ } else {
+ Assert.fail();
+ return;
+ }
+
+ view.setAlpha(startAlpha);
+ view.setVisibility(View.VISIBLE);
+ view.animate()
+ .alpha(endAlpha)
+ .withEndAction(
+ new Runnable() {
+ @Override
+ public void run() {
+ view.setVisibility(visibility);
+ }
+ })
+ .start();
+ }
+
+ private static void blur(Context context, Bitmap image, float blurRadius) {
+ RenderScript renderScript = RenderScript.create(context);
+ ScriptIntrinsicBlur blurScript =
+ ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript));
+ Allocation allocationIn = Allocation.createFromBitmap(renderScript, image);
+ Allocation allocationOut = Allocation.createFromBitmap(renderScript, image);
+ blurScript.setRadius(blurRadius);
+ blurScript.setInput(allocationIn);
+ blurScript.forEach(allocationOut);
+ allocationOut.copyTo(image);
+ }
+
+ @Override
+ public void onSystemUiVisibilityChange(int visibility) {
+ boolean navBarVisible = (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0;
+ videoCallScreenDelegate.onSystemUiVisibilityChange(navBarVisible);
+ }
+
+ protected void onCameraPermissionGranted() {
+ videoCallScreenDelegate.onCameraPermissionGranted();
+ }
+
+ private void checkCameraPermission() {
+ // Checks if user has consent of camera permission and the permission is granted.
+ // If camera permission is revoked, shows system permission dialog.
+ // If camera permission is granted but user doesn't have consent of camera permission
+ // (which means it's first time making video call), shows custom dialog instead. This
+ // will only be shown to user once.
+ if (!VideoUtils.hasCameraPermissionAndAllowedByUser(getContext())) {
+ videoCallScreenDelegate.onCameraPermissionDialogShown();
+ if (!VideoUtils.hasCameraPermission(getContext())) {
+ requestPermissions(new String[] {permission.CAMERA}, CAMERA_PERMISSION_REQUEST_CODE);
+ } else {
+ CameraPermissionDialogFragment.newInstance()
+ .show(getChildFragmentManager(), CAMERA_PERMISSION_DIALOG_FRAMENT_TAG);
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/video/impl/res/color/videocall_button_icon_tint.xml b/java/com/android/incallui/video/impl/res/color/videocall_button_icon_tint.xml
new file mode 100644
index 000000000..b46607b1b
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/color/videocall_button_icon_tint.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="#ff000000" android:state_checked="true"/>
+ <item android:color="#ffffffff"/>
+</selector>
diff --git a/java/com/android/incallui/video/impl/res/drawable-hdpi/ic_switch_camera.png b/java/com/android/incallui/video/impl/res/drawable-hdpi/ic_switch_camera.png
new file mode 100644
index 000000000..b5c6f0a87
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-hdpi/ic_switch_camera.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked.png b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked.png
new file mode 100644
index 000000000..2ab2f21a7
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_disabled.png b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_disabled.png
new file mode 100644
index 000000000..2deaadd76
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_pressed.png b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_pressed.png
new file mode 100644
index 000000000..c4147fa62
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_default.png b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_default.png
new file mode 100644
index 000000000..c59e21504
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_default.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_disabled.png b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_disabled.png
new file mode 100644
index 000000000..95d6824f5
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_pressed.png b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_pressed.png
new file mode 100644
index 000000000..9a525a374
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-mdpi/ic_switch_camera.png b/java/com/android/incallui/video/impl/res/drawable-mdpi/ic_switch_camera.png
new file mode 100644
index 000000000..f3427a02e
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-mdpi/ic_switch_camera.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked.png b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked.png
new file mode 100644
index 000000000..c3ff7b2bb
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_disabled.png b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_disabled.png
new file mode 100644
index 000000000..c75281332
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_pressed.png b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_pressed.png
new file mode 100644
index 000000000..fd16baef7
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_default.png b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_default.png
new file mode 100644
index 000000000..3fe2446e3
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_default.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_disabled.png b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_disabled.png
new file mode 100644
index 000000000..1ff3e7c25
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_pressed.png b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_pressed.png
new file mode 100644
index 000000000..aa7289af1
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xhdpi/ic_switch_camera.png b/java/com/android/incallui/video/impl/res/drawable-xhdpi/ic_switch_camera.png
new file mode 100644
index 000000000..491547189
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xhdpi/ic_switch_camera.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked.png b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked.png
new file mode 100644
index 000000000..799a78ebb
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_disabled.png b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_disabled.png
new file mode 100644
index 000000000..4d5e03320
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_pressed.png b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_pressed.png
new file mode 100644
index 000000000..62cd1a477
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_default.png b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_default.png
new file mode 100644
index 000000000..c68ad909a
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_default.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_disabled.png b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_disabled.png
new file mode 100644
index 000000000..e5c3fc48d
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_pressed.png b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_pressed.png
new file mode 100644
index 000000000..583c3de82
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxhdpi/ic_switch_camera.png b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/ic_switch_camera.png
new file mode 100644
index 000000000..19a9344e9
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/ic_switch_camera.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked.png b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked.png
new file mode 100644
index 000000000..5a7702bbc
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_disabled.png b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_disabled.png
new file mode 100644
index 000000000..a0be8d17d
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_pressed.png b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_pressed.png
new file mode 100644
index 000000000..5671bfa06
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_default.png b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_default.png
new file mode 100644
index 000000000..527b3c47e
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_default.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_disabled.png b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_disabled.png
new file mode 100644
index 000000000..996185890
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_pressed.png b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_pressed.png
new file mode 100644
index 000000000..56295b10f
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxxhdpi/ic_switch_camera.png b/java/com/android/incallui/video/impl/res/drawable-xxxhdpi/ic_switch_camera.png
new file mode 100644
index 000000000..529c0a4d5
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxxhdpi/ic_switch_camera.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable/videocall_background_circle_white.xml b/java/com/android/incallui/video/impl/res/drawable/videocall_background_circle_white.xml
new file mode 100644
index 000000000..ee514c776
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable/videocall_background_circle_white.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="#80888888">
+ <item>
+ <shape
+ android:shape="oval">
+ <solid android:color="@color/incall_button_white"/>
+ </shape>
+ </item>
+</ripple>
diff --git a/java/com/android/incallui/video/impl/res/drawable/videocall_video_button_background.xml b/java/com/android/incallui/video/impl/res/drawable/videocall_video_button_background.xml
new file mode 100644
index 000000000..5e4841327
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable/videocall_video_button_background.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/incall_button_ripple">
+ <item android:id="@android:id/mask">
+ <inset android:inset="5dp">
+ <shape android:shape="oval">
+ <solid android:color="@android:color/white"/>
+ </shape>
+ </inset>
+ </item>
+ <item>
+ <selector>
+ <item
+ android:drawable="@drawable/video_button_bg_checked_pressed"
+ android:state_checked="true"
+ android:state_pressed="true"/>
+ <item
+ android:drawable="@drawable/video_button_bg_checked"
+ android:state_checked="true"/>
+ <item
+ android:drawable="@drawable/video_button_bg_pressed"
+ android:state_pressed="true"/>
+ <item
+ android:drawable="@drawable/video_button_bg_default"/>
+ </selector>
+ </item>
+</ripple>
diff --git a/java/com/android/incallui/video/impl/res/layout-v21/switch_camera_button.xml b/java/com/android/incallui/video/impl/res/layout-v21/switch_camera_button.xml
new file mode 100644
index 000000000..1fb1bb088
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout-v21/switch_camera_button.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ImageButton xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/videocall_switch_video"
+ style="@style/Incall.Button.VideoCall"
+ android:contentDescription="@string/incall_content_description_swap_video"
+ android:src="@drawable/front_back_switch_button_animation"/>
diff --git a/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml b/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml
new file mode 100644
index 000000000..dc663dda1
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/black"
+ android:orientation="vertical">
+
+ <TextureView
+ android:id="@+id/videocall_video_remote"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:importantForAccessibility="no"/>
+
+ <ImageView
+ android:id="@+id/videocall_remote_off_blurred_image_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:scaleType="fitCenter"/>
+
+ <TextView
+ android:gravity="center"
+ android:id="@+id/videocall_remote_video_off"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:accessibilityTraversalBefore="@+id/videocall_speaker_button"
+ android:drawablePadding="8dp"
+ android:drawableTop="@drawable/quantum_ic_videocam_off_white_36"
+ android:padding="64dp"
+ android:text="@string/videocall_remote_video_off"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance"
+ android:visibility="gone"
+ tools:visibility="visible"/>
+
+ <View
+ android:id="@+id/videocall_fullscreen_background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:background="@color/videocall_overlay_background_color"/>
+
+ <TextureView
+ android:id="@+id/videocall_video_preview"
+ android:layout_width="@dimen/videocall_preview_width"
+ android:layout_height="@dimen/videocall_preview_height"
+ android:layout_marginBottom="@dimen/videocall_preview_margin_bottom"
+ android:layout_marginStart="@dimen/videocall_preview_margin_start"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:importantForAccessibility="no"/>
+
+ <ImageView
+ android:id="@+id/videocall_preview_off_blurred_image_view"
+ android:layout_width="@dimen/videocall_preview_width"
+ android:layout_height="@dimen/videocall_preview_height"
+ android:layout_marginBottom="@dimen/videocall_preview_margin_bottom"
+ android:layout_marginStart="@dimen/videocall_preview_margin_start"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:scaleType="center"/>
+
+ <View
+ android:id="@+id/videocall_green_screen_background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:background="@color/videocall_overlay_background_color"/>
+
+ <ImageView
+ android:id="@+id/videocall_video_preview_off_overlay"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@+id/videocall_video_preview"
+ android:layout_alignLeft="@+id/videocall_video_preview"
+ android:layout_alignRight="@+id/videocall_video_preview"
+ android:layout_alignTop="@+id/videocall_video_preview"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_videocam_off_white_36"
+ android:visibility="gone"
+ android:importantForAccessibility="no"
+ tools:visibility="visible"/>
+
+ <ImageView
+ android:id="@+id/videocall_video_preview_mute_overlay"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_alignBottom="@+id/videocall_video_preview"
+ android:layout_alignRight="@+id/videocall_video_preview"
+ android:background="@drawable/videocall_background_circle_white"
+ android:contentDescription="@string/incall_content_description_muted"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_mic_off_black_24"
+ android:visibility="gone"
+ tools:visibility="visible"/>
+
+ <include
+ layout="@layout/videocall_controls"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <FrameLayout
+ android:id="@+id/videocall_on_hold_banner"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"/>
+
+</RelativeLayout>
diff --git a/java/com/android/incallui/video/impl/res/layout/frag_videocall_land.xml b/java/com/android/incallui/video/impl/res/layout/frag_videocall_land.xml
new file mode 100644
index 000000000..2353deea1
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout/frag_videocall_land.xml
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/black">
+
+ <TextureView
+ android:id="@+id/videocall_video_remote"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:importantForAccessibility="no"/>
+
+ <ImageView
+ android:id="@+id/videocall_remote_off_blurred_image_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:scaleType="fitCenter"/>
+
+ <TextView
+ android:gravity="center"
+ android:id="@+id/videocall_remote_video_off"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:accessibilityTraversalBefore="@+id/videocall_speaker_button"
+ android:drawablePadding="8dp"
+ android:drawableTop="@drawable/quantum_ic_videocam_off_white_36"
+ android:padding="64dp"
+ android:text="@string/videocall_remote_video_off"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance"
+ android:visibility="gone"
+ tools:visibility="visible"/>
+
+ <View
+ android:id="@+id/videocall_fullscreen_background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:background="@color/videocall_overlay_background_color"/>
+
+ <TextureView
+ android:id="@+id/videocall_video_preview"
+ android:layout_width="@dimen/videocall_preview_width"
+ android:layout_height="@dimen/videocall_preview_height"
+ android:layout_marginEnd="@dimen/videocall_preview_margin_end"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentEnd="true"
+ android:importantForAccessibility="no"/>
+
+ <ImageView
+ android:id="@+id/videocall_preview_off_blurred_image_view"
+ android:layout_width="@dimen/videocall_preview_width"
+ android:layout_height="@dimen/videocall_preview_height"
+ android:layout_marginEnd="@dimen/videocall_preview_margin_end"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentEnd="true"
+ android:scaleType="center"/>
+
+ <View
+ android:id="@+id/videocall_green_screen_background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:background="@color/videocall_overlay_background_color"/>
+
+ <ImageView
+ android:id="@+id/videocall_video_preview_off_overlay"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@+id/videocall_video_preview"
+ android:layout_alignLeft="@+id/videocall_video_preview"
+ android:layout_alignRight="@+id/videocall_video_preview"
+ android:layout_alignTop="@+id/videocall_video_preview"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_videocam_off_white_36"
+ android:visibility="gone"
+ android:importantForAccessibility="no"
+ tools:visibility="visible"/>
+
+ <ImageView
+ android:id="@+id/videocall_video_preview_mute_overlay"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_alignBottom="@+id/videocall_video_preview"
+ android:layout_alignRight="@+id/videocall_video_preview"
+ android:background="@drawable/videocall_background_circle_white"
+ android:contentDescription="@string/incall_content_description_muted"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_mic_off_black_24"
+ android:visibility="gone"
+ tools:visibility="visible"/>
+
+ <include
+ layout="@layout/videocall_controls_land"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <FrameLayout
+ android:id="@+id/videocall_on_hold_banner"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"/>
+
+</RelativeLayout>
diff --git a/java/com/android/incallui/video/impl/res/layout/switch_camera_button.xml b/java/com/android/incallui/video/impl/res/layout/switch_camera_button.xml
new file mode 100644
index 000000000..87c2e1b6c
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout/switch_camera_button.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ImageButton xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/videocall_switch_video"
+ style="@style/Incall.Button.VideoCall"
+ android:contentDescription="@string/incall_content_description_swap_video"
+ android:src="@drawable/ic_switch_camera"/>
diff --git a/java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml b/java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml
new file mode 100644
index 000000000..ad984f36e
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:paddingTop="16dp"
+ android:orientation="vertical">
+
+ <include
+ layout="@layout/incall_contactgrid_top_row"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <!-- We have to keep deprecated singleLine to allow long text being truncated with ellipses.
+ b/31396406 -->
+ <com.android.incallui.autoresizetext.AutoResizeTextView
+ android:id="@id/contactgrid_contact_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance.Large"
+ app:autoResizeText_minTextSize="28sp"
+ tools:text="Jake Peralta"
+ tools:ignore="Deprecated"/>
+
+ <include
+ layout="@layout/incall_contactgrid_bottom_row"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+</LinearLayout>
diff --git a/java/com/android/incallui/video/impl/res/layout/videocall_controls.xml b/java/com/android/incallui/video/impl/res/layout/videocall_controls.xml
new file mode 100644
index 000000000..b3141bdf3
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout/videocall_controls.xml
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/videocall_video_controls_container"
+ android:fitsSystemWindows="true"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <include
+ android:id="@+id/incall_contact_grid"
+ layout="@layout/video_contact_grid"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginStart="24dp"
+ android:layout_marginEnd="24dp"/>
+
+ <!-- This placeholder matches the position of the preview UI and is used to
+ anchor video buttons. This is needed in greenscreen mode when the
+ preview is fullscreen but we want the controls to be positioned as
+ normal. -->
+ <Space
+ android:id="@+id/videocall_video_preview_placeholder"
+ android:layout_width="@dimen/videocall_preview_width"
+ android:layout_height="@dimen/videocall_preview_height"
+ android:layout_marginBottom="@dimen/videocall_preview_margin_bottom"
+ android:layout_marginStart="@dimen/videocall_preview_margin_start"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:visibility="invisible"/>
+
+ <LinearLayout
+ android:id="@+id/videocall_video_controls"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/videocall_video_preview_placeholder"
+ android:layout_alignEnd="@+id/videocall_video_preview_placeholder"
+ android:layout_alignStart="@+id/videocall_video_preview_placeholder"
+ android:gravity="center_horizontal"
+ android:orientation="vertical"
+ android:visibility="invisible"
+ tools:visibility="visible">
+ <com.android.incallui.video.impl.CheckableImageButton
+ android:id="@+id/videocall_speaker_button"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginBottom="@dimen/videocall_button_spacing"
+ android:checked="true"
+ android:src="@drawable/quantum_ic_volume_up_white_36"
+ app:contentDescriptionChecked="@string/incall_content_description_speaker"
+ app:contentDescriptionUnchecked="@string/incall_content_description_earpiece"
+ />
+ <com.android.incallui.video.impl.CheckableImageButton
+ android:id="@+id/videocall_mute_button"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginBottom="@dimen/videocall_button_spacing"
+ android:src="@drawable/quantum_ic_mic_off_white_36"
+ app:contentDescriptionChecked="@string/incall_content_description_muted"
+ app:contentDescriptionUnchecked="@string/incall_content_description_unmuted"
+ />
+ <com.android.incallui.video.impl.CheckableImageButton
+ android:id="@+id/videocall_mute_video"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginBottom="@dimen/videocall_button_spacing"
+ android:src="@drawable/quantum_ic_videocam_off_white_36"
+ app:contentDescriptionChecked="@string/incall_content_description_video_off"
+ app:contentDescriptionUnchecked="@string/incall_content_description_video_on"
+ />
+ <include
+ layout="@layout/switch_camera_button"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginBottom="@dimen/videocall_button_spacing"/>
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/videocall_switch_controls"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="36dp"
+ android:layout_marginEnd="24dp"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentEnd="true">
+ <ImageButton
+ android:id="@+id/videocall_switch_on_hold"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:contentDescription="@string/incall_content_description_swap_calls"
+ android:src="@drawable/quantum_ic_swap_calls_white_36"
+ android:visibility="gone"
+ tools:visibility="visible"
+ />
+ </FrameLayout>
+
+ <ImageButton
+ android:id="@+id/videocall_end_call"
+ style="@style/Incall.Button.End"
+ android:layout_marginBottom="36dp"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"
+ android:contentDescription="@string/incall_content_description_end_call"
+ android:visibility="visible"/>
+
+</RelativeLayout>
diff --git a/java/com/android/incallui/video/impl/res/layout/videocall_controls_land.xml b/java/com/android/incallui/video/impl/res/layout/videocall_controls_land.xml
new file mode 100644
index 000000000..d71b3c00e
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout/videocall_controls_land.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/videocall_video_controls_container"
+ android:fitsSystemWindows="true"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <include
+ android:id="@+id/incall_contact_grid"
+ layout="@layout/video_contact_grid"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginStart="24dp"
+ android:layout_marginEnd="24dp"/>
+
+ <!-- This placeholder matches the position of the preview UI and is used to
+ anchor video buttons. This is needed in greenscreen mode when the
+ preview is fullscreen but we want the controls to be positioned as
+ normal. -->
+ <Space
+ android:id="@+id/videocall_video_preview_placeholder"
+ android:layout_width="@dimen/videocall_preview_width"
+ android:layout_height="@dimen/videocall_preview_height"
+ android:layout_marginEnd="@dimen/videocall_preview_margin_end"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentEnd="true"
+ android:visibility="invisible"/>
+
+ <LinearLayout
+ android:id="@+id/videocall_video_controls"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@+id/videocall_video_preview_placeholder"
+ android:layout_alignTop="@+id/videocall_video_preview_placeholder"
+ android:layout_toStartOf="@+id/videocall_video_preview_placeholder"
+ android:gravity="center_horizontal"
+ android:orientation="horizontal"
+ android:visibility="invisible"
+ tools:visibility="visible">
+ <com.android.incallui.video.impl.CheckableImageButton
+ android:id="@+id/videocall_speaker_button"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginEnd="24dp"
+ android:checked="true"
+ android:src="@drawable/quantum_ic_volume_up_white_36"
+ app:contentDescriptionChecked="@string/incall_content_description_speaker"
+ app:contentDescriptionUnchecked="@string/incall_content_description_earpiece"
+ />
+ <com.android.incallui.video.impl.CheckableImageButton
+ android:id="@+id/videocall_mute_button"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginEnd="24dp"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_mic_off_white_36"
+ app:contentDescriptionChecked="@string/incall_content_description_muted"
+ app:contentDescriptionUnchecked="@string/incall_content_description_unmuted"
+ />
+ <com.android.incallui.video.impl.CheckableImageButton
+ android:id="@+id/videocall_mute_video"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginEnd="24dp"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_videocam_off_white_36"
+ app:contentDescriptionChecked="@string/incall_content_description_video_off"
+ app:contentDescriptionUnchecked="@string/incall_content_description_video_on"
+ />
+ <include
+ layout="@layout/switch_camera_button"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginEnd="24dp"/>
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/videocall_switch_controls"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="36dp"
+ android:layout_marginEnd="36dp"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentTop="true">
+ <ImageButton
+ android:id="@+id/videocall_switch_on_hold"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:contentDescription="@string/incall_content_description_swap_calls"
+ android:src="@drawable/quantum_ic_swap_calls_white_36"
+ android:visibility="gone"
+ tools:visibility="visible"
+ />
+ </FrameLayout>
+
+ <ImageButton
+ android:id="@+id/videocall_end_call"
+ style="@style/Incall.Button.End"
+ android:layout_marginEnd="36dp"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:contentDescription="@string/incall_content_description_end_call"
+ android:visibility="visible"
+ tools:visibility="visible"/>
+
+</RelativeLayout>
diff --git a/java/com/android/incallui/video/impl/res/values-h580dp/dimens.xml b/java/com/android/incallui/video/impl/res/values-h580dp/dimens.xml
new file mode 100644
index 000000000..b1a86a0fa
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values-h580dp/dimens.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="videocall_button_spacing">16dp</dimen>
+ <dimen name="videocall_button_size">72dp</dimen>
+ <dimen name="videocall_preview_width">88dp</dimen>
+ <dimen name="videocall_preview_height">88dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/video/impl/res/values-w460dp/dimens.xml b/java/com/android/incallui/video/impl/res/values-w460dp/dimens.xml
new file mode 100644
index 000000000..b1a86a0fa
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values-w460dp/dimens.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="videocall_button_spacing">16dp</dimen>
+ <dimen name="videocall_button_size">72dp</dimen>
+ <dimen name="videocall_preview_width">88dp</dimen>
+ <dimen name="videocall_preview_height">88dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/video/impl/res/values/attrs.xml b/java/com/android/incallui/video/impl/res/values/attrs.xml
new file mode 100644
index 000000000..e4cd8af89
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values/attrs.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <declare-styleable name="CheckableImageButton">
+ <attr name="android:checked"/>
+ <attr name="contentDescriptionChecked" format="reference|string"/>
+ <attr name="contentDescriptionUnchecked" format="reference|string"/>
+ </declare-styleable>
+</resources>
diff --git a/java/com/android/incallui/video/impl/res/values/dimens.xml b/java/com/android/incallui/video/impl/res/values/dimens.xml
new file mode 100644
index 000000000..45860036f
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values/dimens.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="videocall_preview_width">72dp</dimen>
+ <dimen name="videocall_preview_height">72dp</dimen>
+ <dimen name="videocall_preview_margin_bottom">24dp</dimen>
+ <dimen name="videocall_preview_margin_start">24dp</dimen>
+ <dimen name="videocall_preview_margin_end">24dp</dimen>
+ <dimen name="videocall_button_spacing">8dp</dimen>
+ <dimen name="videocall_button_size">60dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/video/impl/res/values/strings.xml b/java/com/android/incallui/video/impl/res/values/strings.xml
new file mode 100644
index 000000000..2b72b8004
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Text indicates the video from remote party is off. [CHAR LIMIT=40] -->
+ <string name="videocall_remote_video_off">Their video is off</string>
+
+ <!-- Text indicates the video from remote party is on. [CHAR LIMIT=40] -->
+ <string name="videocall_remote_video_on">Their video is on</string>
+
+ <!-- Text indicates the call is held by remote party. [CHAR LIMIT=20] -->
+ <string name="videocall_remotely_held">Call on hold</string>
+
+ <!-- Text indicates the call is resumed from held by remote party. [CHAR LIMIT=20] -->
+ <string name="videocall_remotely_resumed">Call resumed</string>
+
+ <!-- Title of dialog to ask user for camera permission. [CHAR LIMIT=30] -->
+ <string name="camera_permission_dialog_title">Allow video?</string>
+
+ <!-- Message of dialog to ask user for camera permission. [CHAR LIMIT=100] -->
+ <string name="camera_permission_dialog_message">The Phone app wants to use your camera for video calls.</string>
+
+ <!-- Text of button to be confirmed for camera permission by user. [CHAR LIMIT=20] -->
+ <string name="camera_permission_dialog_positive_button">Allow</string>
+
+ <!-- Text of button to be declined for camera permission by user. [CHAR LIMIT=20] -->
+ <string name="camera_permission_dialog_negative_button">Deny</string>
+
+</resources>
diff --git a/java/com/android/incallui/video/impl/res/values/styles.xml b/java/com/android/incallui/video/impl/res/values/styles.xml
new file mode 100644
index 000000000..b94400875
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values/styles.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="Incall.Button.VideoCall" parent="Widget.AppCompat.ImageButton">
+ <item name="android:background">@drawable/videocall_video_button_background</item>
+ <item name="android:scaleType">center</item>
+ <item name="android:tint">@color/videocall_button_icon_tint</item>
+ <item name="android:tintMode">src_atop</item>
+ <item name="android:stateListAnimator">@animator/disabled_alpha</item>
+ </style>
+</resources>
diff --git a/java/com/android/incallui/video/protocol/VideoCallScreen.java b/java/com/android/incallui/video/protocol/VideoCallScreen.java
new file mode 100644
index 000000000..0eaf692e2
--- /dev/null
+++ b/java/com/android/incallui/video/protocol/VideoCallScreen.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.video.protocol;
+
+import android.support.v4.app.Fragment;
+
+/** Interface for call video call module. */
+public interface VideoCallScreen {
+
+ void showVideoViews(boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld);
+
+ void onLocalVideoDimensionsChanged();
+
+ void onLocalVideoOrientationChanged();
+
+ void onRemoteVideoDimensionsChanged();
+
+ void updateFullscreenAndGreenScreenMode(
+ boolean shouldShowFullscreen, boolean shouldShowGreenScreen);
+
+ Fragment getVideoCallScreenFragment();
+}
diff --git a/java/com/android/incallui/video/protocol/VideoCallScreenDelegate.java b/java/com/android/incallui/video/protocol/VideoCallScreenDelegate.java
new file mode 100644
index 000000000..bbd86ee6a
--- /dev/null
+++ b/java/com/android/incallui/video/protocol/VideoCallScreenDelegate.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.video.protocol;
+
+import android.content.Context;
+import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
+
+/** Callbacks from the module out to the container. */
+public interface VideoCallScreenDelegate {
+
+ void initVideoCallScreenDelegate(Context context, VideoCallScreen videoCallScreen);
+
+ void onVideoCallScreenUiReady();
+
+ void onVideoCallScreenUiUnready();
+
+ void cancelAutoFullScreen();
+
+ void resetAutoFullscreenTimer();
+
+ void onSystemUiVisibilityChange(boolean visible);
+
+ void onCameraPermissionGranted();
+
+ boolean shouldShowCameraPermissionDialog();
+
+ void onCameraPermissionDialogShown();
+
+ VideoSurfaceTexture getLocalVideoSurfaceTexture();
+
+ VideoSurfaceTexture getRemoteVideoSurfaceTexture();
+
+ int getDeviceOrientation();
+}
diff --git a/java/com/android/incallui/video/protocol/VideoCallScreenDelegateFactory.java b/java/com/android/incallui/video/protocol/VideoCallScreenDelegateFactory.java
new file mode 100644
index 000000000..285857a23
--- /dev/null
+++ b/java/com/android/incallui/video/protocol/VideoCallScreenDelegateFactory.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.video.protocol;
+
+/** Callbacks from the module out to the container. */
+public interface VideoCallScreenDelegateFactory {
+
+ VideoCallScreenDelegate newVideoCallScreenDelegate();
+}
diff --git a/java/com/android/incallui/videosurface/bindings/VideoSurfaceBindings.java b/java/com/android/incallui/videosurface/bindings/VideoSurfaceBindings.java
new file mode 100644
index 000000000..96fccb451
--- /dev/null
+++ b/java/com/android/incallui/videosurface/bindings/VideoSurfaceBindings.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.videosurface.bindings;
+
+import android.view.TextureView;
+import com.android.incallui.videosurface.impl.VideoScale;
+import com.android.incallui.videosurface.impl.VideoSurfaceTextureImpl;
+import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
+
+/** Bindings for video surface module. */
+public class VideoSurfaceBindings {
+
+ public static VideoSurfaceTexture createLocalVideoSurfaceTexture() {
+ return new VideoSurfaceTextureImpl(VideoSurfaceTexture.SURFACE_TYPE_LOCAL);
+ }
+
+ public static VideoSurfaceTexture createRemoteVideoSurfaceTexture() {
+ return new VideoSurfaceTextureImpl(VideoSurfaceTexture.SURFACE_TYPE_REMOTE);
+ }
+
+ public static void scaleVideoAndFillView(
+ TextureView textureView, float videoWidth, float videoHeight, float rotationDegrees) {
+ VideoScale.scaleVideoAndFillView(textureView, videoWidth, videoHeight, rotationDegrees);
+ }
+
+ public static void scaleVideoMaintainingAspectRatio(
+ TextureView textureView, int videoWidth, int videoHeight) {
+ VideoScale.scaleVideoMaintainingAspectRatio(textureView, videoWidth, videoHeight);
+ }
+}
diff --git a/java/com/android/incallui/videosurface/impl/VideoScale.java b/java/com/android/incallui/videosurface/impl/VideoScale.java
new file mode 100644
index 000000000..1444f5900
--- /dev/null
+++ b/java/com/android/incallui/videosurface/impl/VideoScale.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.videosurface.impl;
+
+import android.graphics.Matrix;
+import android.view.TextureView;
+import com.android.dialer.common.LogUtil;
+
+/** Utilities to scale the preview and remote video. */
+public class VideoScale {
+ /**
+ * Scales the video in the given view such that the video takes up the entire view. To maintain
+ * aspect ratio the video will be scaled to be larger than the view.
+ */
+ public static void scaleVideoAndFillView(
+ TextureView textureView, float videoWidth, float videoHeight, float rotationDegrees) {
+ float viewWidth = textureView.getWidth();
+ float viewHeight = textureView.getHeight();
+ float viewAspectRatio = viewWidth / viewHeight;
+ float videoAspectRatio = videoWidth / videoHeight;
+ float scaleWidth = 1.0f;
+ float scaleHeight = 1.0f;
+
+ if (viewAspectRatio > videoAspectRatio) {
+ // Scale to exactly fit the width of the video. The top and bottom will be cropped.
+ float scaleFactor = viewWidth / videoWidth;
+ float desiredScaledHeight = videoHeight * scaleFactor;
+ scaleHeight = desiredScaledHeight / viewHeight;
+ } else {
+ // Scale to exactly fit the height of the video. The sides will be cropped.
+ float scaleFactor = viewHeight / videoHeight;
+ float desiredScaledWidth = videoWidth * scaleFactor;
+ scaleWidth = desiredScaledWidth / viewWidth;
+ }
+
+ if (rotationDegrees == 90.0f || rotationDegrees == 270.0f) {
+ // We're in landscape mode but the camera feed is still drawing in portrait mode. Normally,
+ // scale of 1.0 means that the video feed stretches to fit the view. In this case the X axis
+ // is scaled to fit the height and the Y axis is scaled to fit the width.
+ float scaleX = scaleWidth;
+ float scaleY = scaleHeight;
+ scaleWidth = viewHeight / viewWidth * scaleY;
+ scaleHeight = viewWidth / viewHeight * scaleX;
+
+ // This flips the view vertically. Without this the camera feed would be upside down.
+ scaleWidth = scaleWidth * -1.0f;
+ // This flips the view horizontally. Without this the camera feed would be mirrored (left
+ // side would appear on right).
+ scaleHeight = scaleHeight * -1.0f;
+ }
+
+ LogUtil.i(
+ "VideoScale.scaleVideoAndFillView",
+ "view: %f x %f, video: %f x %f scale: %f x %f, rotation: %f",
+ viewWidth,
+ viewHeight,
+ videoWidth,
+ videoHeight,
+ scaleWidth,
+ scaleHeight,
+ rotationDegrees);
+
+ Matrix transform = new Matrix();
+ transform.setScale(
+ scaleWidth,
+ scaleHeight,
+ // This performs the scaling from the horizontal middle of the view.
+ viewWidth / 2.0f,
+ // This perform the scaling from vertical middle of the view.
+ viewHeight / 2.0f);
+ if (rotationDegrees != 0) {
+ transform.postRotate(rotationDegrees, viewWidth / 2.0f, viewHeight / 2.0f);
+ }
+ textureView.setTransform(transform);
+ }
+
+ /**
+ * Scales the video in the given view such that all of the video is visible. This will result in
+ * black bars on the top and bottom or the sides of the video.
+ */
+ public static void scaleVideoMaintainingAspectRatio(
+ TextureView textureView, int videoWidth, int videoHeight) {
+ int viewWidth = textureView.getWidth();
+ int viewHeight = textureView.getHeight();
+ float scaleWidth = 1.0f;
+ float scaleHeight = 1.0f;
+
+ if (viewWidth > viewHeight) {
+ // Landscape layout.
+ if (viewHeight * videoWidth > viewWidth * videoHeight) {
+ // Current display height is too much. Correct it.
+ int desiredHeight = viewWidth * videoHeight / videoWidth;
+ scaleWidth = (float) desiredHeight / (float) viewHeight;
+ } else if (viewHeight * videoWidth < viewWidth * videoHeight) {
+ // Current display width is too much. Correct it.
+ int desiredWidth = viewHeight * videoWidth / videoHeight;
+ scaleWidth = (float) desiredWidth / (float) viewWidth;
+ }
+ } else {
+ // Portrait layout.
+ if (viewHeight * videoWidth > viewWidth * videoHeight) {
+ // Current display height is too much. Correct it.
+ int desiredHeight = viewWidth * videoHeight / videoWidth;
+ scaleHeight = (float) desiredHeight / (float) viewHeight;
+ } else if (viewHeight * videoWidth < viewWidth * videoHeight) {
+ // Current display width is too much. Correct it.
+ int desiredWidth = viewHeight * videoWidth / videoHeight;
+ scaleHeight = (float) desiredWidth / (float) viewWidth;
+ }
+ }
+
+ LogUtil.i(
+ "VideoScale.scaleVideoMaintainingAspectRatio",
+ "view: %d x %d, video: %d x %d scale: %f x %f",
+ viewWidth,
+ viewHeight,
+ videoWidth,
+ videoHeight,
+ scaleWidth,
+ scaleHeight);
+ Matrix transform = new Matrix();
+ transform.setScale(
+ scaleWidth,
+ scaleHeight,
+ // This performs the scaling from the horizontal middle of the view.
+ viewWidth / 2.0f,
+ // This perform the scaling from vertical middle of the view.
+ viewHeight / 2.0f);
+ textureView.setTransform(transform);
+ }
+
+ private VideoScale() {}
+}
diff --git a/java/com/android/incallui/videosurface/impl/VideoSurfaceTextureImpl.java b/java/com/android/incallui/videosurface/impl/VideoSurfaceTextureImpl.java
new file mode 100644
index 000000000..21160cadb
--- /dev/null
+++ b/java/com/android/incallui/videosurface/impl/VideoSurfaceTextureImpl.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.videosurface.impl;
+
+import android.graphics.Point;
+import android.graphics.SurfaceTexture;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.View;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.videosurface.protocol.VideoSurfaceDelegate;
+import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * Represents a {@link TextureView} and its associated {@link SurfaceTexture} and {@link Surface}.
+ * Used to manage the lifecycle of these objects across device orientation changes.
+ */
+public class VideoSurfaceTextureImpl implements VideoSurfaceTexture {
+ @SurfaceType private final int surfaceType;
+ private VideoSurfaceDelegate delegate;
+ private TextureView textureView;
+ private Surface savedSurface;
+ private SurfaceTexture savedSurfaceTexture;
+ private Point surfaceDimensions;
+ private Point sourceVideoDimensions;
+ private boolean isDoneWithSurface;
+
+ public VideoSurfaceTextureImpl(@SurfaceType int surfaceType) {
+ this.surfaceType = surfaceType;
+ }
+
+ @Override
+ public void setDelegate(VideoSurfaceDelegate delegate) {
+ LogUtil.i("VideoSurfaceTextureImpl.setDelegate", "delegate: " + delegate + " " + toString());
+ this.delegate = delegate;
+ }
+
+ @Override
+ public int getSurfaceType() {
+ return surfaceType;
+ }
+
+ @Override
+ public Surface getSavedSurface() {
+ return savedSurface;
+ }
+
+ @Override
+ public void setSurfaceDimensions(Point surfaceDimensions) {
+ LogUtil.i(
+ "VideoSurfaceTextureImpl.setSurfaceDimensions",
+ "surfaceDimensions: " + surfaceDimensions + " " + toString());
+ this.surfaceDimensions = surfaceDimensions;
+ if (surfaceDimensions != null && savedSurfaceTexture != null) {
+ savedSurfaceTexture.setDefaultBufferSize(surfaceDimensions.x, surfaceDimensions.y);
+ }
+ }
+
+ @Override
+ public Point getSurfaceDimensions() {
+ return surfaceDimensions;
+ }
+
+ @Override
+ public void setSourceVideoDimensions(Point sourceVideoDimensions) {
+ this.sourceVideoDimensions = sourceVideoDimensions;
+ }
+
+ @Override
+ public Point getSourceVideoDimensions() {
+ return sourceVideoDimensions;
+ }
+
+ @Override
+ public void attachToTextureView(TextureView textureView) {
+ if (this.textureView == textureView) {
+ return;
+ }
+ LogUtil.i("VideoSurfaceTextureImpl.attachToTextureView", toString());
+
+ if (this.textureView != null) {
+ this.textureView.setOnClickListener(null);
+ // Don't clear the surface texture listener. This is important because our listener prevents
+ // the surface from being released so that it can be reused later.
+ }
+
+ this.textureView = textureView;
+ textureView.setSurfaceTextureListener(new SurfaceTextureListener());
+ textureView.setOnClickListener(new OnClickListener());
+
+ boolean areSameSurfaces = Objects.equals(savedSurfaceTexture, textureView.getSurfaceTexture());
+ LogUtil.i("VideoSurfaceTextureImpl.attachToTextureView", "areSameSurfaces: " + areSameSurfaces);
+ if (savedSurfaceTexture != null && !areSameSurfaces) {
+ textureView.setSurfaceTexture(savedSurfaceTexture);
+ if (surfaceDimensions != null && createSurface(surfaceDimensions.x, surfaceDimensions.y)) {
+ onSurfaceCreated();
+ }
+ }
+ isDoneWithSurface = false;
+ }
+
+ @Override
+ public void setDoneWithSurface() {
+ LogUtil.i("VideoSurfaceTextureImpl.setDoneWithSurface", toString());
+ isDoneWithSurface = true;
+ if (textureView != null && textureView.isAvailable()) {
+ return;
+ }
+ if (savedSurface != null) {
+ onSurfaceReleased();
+ savedSurface.release();
+ savedSurface = null;
+ }
+ if (savedSurfaceTexture != null) {
+ savedSurfaceTexture.release();
+ savedSurfaceTexture = null;
+ }
+ }
+
+ private boolean createSurface(int width, int height) {
+ LogUtil.i(
+ "VideoSurfaceTextureImpl.createSurface",
+ "width: " + width + ", height: " + height + " " + toString());
+ if (savedSurfaceTexture != null) {
+ savedSurfaceTexture.setDefaultBufferSize(width, height);
+ savedSurface = new Surface(savedSurfaceTexture);
+ return true;
+ }
+ return false;
+ }
+
+ private void onSurfaceCreated() {
+ if (delegate != null) {
+ delegate.onSurfaceCreated(this);
+ } else {
+ LogUtil.e("VideoSurfaceTextureImpl.onSurfaceCreated", "delegate is null. " + toString());
+ }
+ }
+
+ private void onSurfaceReleased() {
+ if (delegate != null) {
+ delegate.onSurfaceReleased(this);
+ } else {
+ LogUtil.e("VideoSurfaceTextureImpl.onSurfaceReleased", "delegate is null. " + toString());
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US,
+ "VideoSurfaceTextureImpl<%s%s%s%s>",
+ (surfaceType == SURFACE_TYPE_LOCAL ? "local, " : "remote, "),
+ (savedSurface == null ? "no-surface, " : ""),
+ (savedSurfaceTexture == null ? "no-texture, " : ""),
+ (surfaceDimensions == null
+ ? "(-1 x -1)"
+ : (surfaceDimensions.x + " x " + surfaceDimensions.y)));
+ }
+
+ private class SurfaceTextureListener implements TextureView.SurfaceTextureListener {
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture newSurfaceTexture, int width, int height) {
+ LogUtil.i(
+ "SurfaceTextureListener.onSurfaceTextureAvailable",
+ "newSurfaceTexture: "
+ + newSurfaceTexture
+ + " "
+ + VideoSurfaceTextureImpl.this.toString());
+
+ // Where there is no saved {@link SurfaceTexture} available, use the newly created one.
+ // If a saved {@link SurfaceTexture} is available, we are re-creating after an
+ // orientation change.
+ boolean surfaceCreated;
+ if (savedSurfaceTexture == null) {
+ savedSurfaceTexture = newSurfaceTexture;
+ surfaceCreated = createSurface(width, height);
+ } else {
+ // A saved SurfaceTexture was found.
+ LogUtil.i(
+ "SurfaceTextureListener.onSurfaceTextureAvailable", "replacing with cached surface...");
+ textureView.setSurfaceTexture(savedSurfaceTexture);
+ surfaceCreated = true;
+ }
+
+ // Inform the delegate that the surface is available.
+ if (surfaceCreated) {
+ onSurfaceCreated();
+ }
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture destroyedSurfaceTexture) {
+ LogUtil.i(
+ "SurfaceTextureListener.onSurfaceTextureDestroyed",
+ "destroyedSurfaceTexture: "
+ + destroyedSurfaceTexture
+ + " "
+ + VideoSurfaceTextureImpl.this.toString());
+ if (delegate != null) {
+ delegate.onSurfaceDestroyed(VideoSurfaceTextureImpl.this);
+ } else {
+ LogUtil.e("SurfaceTextureListener.onSurfaceTextureDestroyed", "delegate is null");
+ }
+
+ if (isDoneWithSurface) {
+ onSurfaceReleased();
+ if (savedSurface != null) {
+ savedSurface.release();
+ savedSurface = null;
+ }
+ }
+ return isDoneWithSurface;
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {}
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {}
+ }
+
+ private class OnClickListener implements View.OnClickListener {
+ @Override
+ public void onClick(View view) {
+ if (delegate != null) {
+ delegate.onSurfaceClick(VideoSurfaceTextureImpl.this);
+ } else {
+ LogUtil.e("OnClickListener.onClick", "delegate is null");
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/videosurface/protocol/VideoSurfaceDelegate.java b/java/com/android/incallui/videosurface/protocol/VideoSurfaceDelegate.java
new file mode 100644
index 000000000..8fa585a72
--- /dev/null
+++ b/java/com/android/incallui/videosurface/protocol/VideoSurfaceDelegate.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.videosurface.protocol;
+
+/** Callbacks from the video surface. */
+public interface VideoSurfaceDelegate {
+
+ void onSurfaceCreated(VideoSurfaceTexture videoCallSurface);
+
+ void onSurfaceReleased(VideoSurfaceTexture videoCallSurface);
+
+ void onSurfaceDestroyed(VideoSurfaceTexture videoCallSurface);
+
+ void onSurfaceClick(VideoSurfaceTexture videoCallSurface);
+}
diff --git a/java/com/android/incallui/videosurface/protocol/VideoSurfaceTexture.java b/java/com/android/incallui/videosurface/protocol/VideoSurfaceTexture.java
new file mode 100644
index 000000000..411b45f56
--- /dev/null
+++ b/java/com/android/incallui/videosurface/protocol/VideoSurfaceTexture.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.videosurface.protocol;
+
+import android.graphics.Point;
+import android.support.annotation.IntDef;
+import android.view.Surface;
+import android.view.TextureView;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Represents a surface texture for a video feed. */
+public interface VideoSurfaceTexture {
+
+ /** Whether this represents the preview or remote display. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ SURFACE_TYPE_LOCAL,
+ SURFACE_TYPE_REMOTE,
+ })
+ @interface SurfaceType {}
+
+ int SURFACE_TYPE_LOCAL = 1;
+ int SURFACE_TYPE_REMOTE = 2;
+
+ void setDelegate(VideoSurfaceDelegate delegate);
+
+ int getSurfaceType();
+
+ Surface getSavedSurface();
+
+ void setSurfaceDimensions(Point surfaceDimensions);
+
+ Point getSurfaceDimensions();
+
+ void setSourceVideoDimensions(Point sourceVideoDimensions);
+
+ Point getSourceVideoDimensions();
+
+ void attachToTextureView(TextureView textureView);
+
+ void setDoneWithSurface();
+}
diff --git a/java/com/android/incallui/wifi/AndroidManifest.xml b/java/com/android/incallui/wifi/AndroidManifest.xml
new file mode 100644
index 000000000..843f8f3e6
--- /dev/null
+++ b/java/com/android/incallui/wifi/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.incallui.wifi">
+</manifest>
diff --git a/java/com/android/incallui/wifi/EnableWifiCallingPrompt.java b/java/com/android/incallui/wifi/EnableWifiCallingPrompt.java
new file mode 100644
index 000000000..85603bfb1
--- /dev/null
+++ b/java/com/android/incallui/wifi/EnableWifiCallingPrompt.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.wifi;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.support.annotation.NonNull;
+import android.telecom.DisconnectCause;
+import android.util.Pair;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+
+/** Prompts the user to enable Wi-Fi calling. */
+public class EnableWifiCallingPrompt {
+ // This is a hidden constant in android.telecom.DisconnectCause. Telecom sets this as a disconnect
+ // reason if it wants us to prompt the user to enable Wi-Fi calling. In Android-O we might
+ // consider using a more explicit way to signal this.
+ private static final String REASON_WIFI_ON_BUT_WFC_OFF = "REASON_WIFI_ON_BUT_WFC_OFF";
+ private static final String ACTION_WIFI_CALLING_SETTINGS =
+ "android.settings.WIFI_CALLING_SETTINGS";
+ private static final String ANDROID_SETTINGS_PACKAGE = "com.android.settings";
+
+ public static boolean shouldShowPrompt(@NonNull DisconnectCause cause) {
+ Assert.isNotNull(cause);
+ if (cause.getReason() != null && cause.getReason().startsWith(REASON_WIFI_ON_BUT_WFC_OFF)) {
+ LogUtil.i(
+ "EnableWifiCallingPrompt.shouldShowPrompt",
+ "showing prompt for disconnect cause: %s",
+ cause);
+ return true;
+ }
+ return false;
+ }
+
+ @NonNull
+ public static Pair<Dialog, CharSequence> createDialog(
+ final @NonNull Context context, @NonNull DisconnectCause cause) {
+ Assert.isNotNull(context);
+ Assert.isNotNull(cause);
+ CharSequence message = cause.getDescription();
+ Dialog dialog =
+ new AlertDialog.Builder(context)
+ .setMessage(message)
+ .setPositiveButton(
+ R.string.incall_enable_wifi_calling_button,
+ new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ openWifiCallingSettings(context);
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, null)
+ .create();
+ return new Pair<Dialog, CharSequence>(dialog, message);
+ }
+
+ private static void openWifiCallingSettings(@NonNull Context context) {
+ LogUtil.i("EnableWifiCallingPrompt.openWifiCallingSettings", "opening settings");
+ context.startActivity(
+ new Intent(ACTION_WIFI_CALLING_SETTINGS).setPackage(ANDROID_SETTINGS_PACKAGE));
+ }
+
+ private EnableWifiCallingPrompt() {}
+}
diff --git a/java/com/android/incallui/wifi/res/values/strings.xml b/java/com/android/incallui/wifi/res/values/strings.xml
new file mode 100644
index 000000000..1b52b9fdc
--- /dev/null
+++ b/java/com/android/incallui/wifi/res/values/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- Button to enable Wi-Fi calling. This is displayed in a dialog after a phone call disconnects
+ because there is no cellular service.
+ [CHAR LIMIT=20] -->
+ <string name="incall_enable_wifi_calling_button">Enable</string>
+
+</resources>