1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car.carlauncher.calmmode; 18 19 import static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK; 20 21 import android.os.Build; 22 import android.os.Bundle; 23 import android.os.Handler; 24 import android.os.Looper; 25 import android.util.Log; 26 import android.view.LayoutInflater; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.view.WindowInsets; 30 import android.widget.ImageView; 31 import android.widget.TextView; 32 33 import androidx.annotation.NonNull; 34 import androidx.annotation.Nullable; 35 import androidx.annotation.VisibleForTesting; 36 import androidx.core.animation.Animator; 37 import androidx.core.animation.AnimatorInflater; 38 import androidx.core.animation.AnimatorSet; 39 import androidx.fragment.app.Fragment; 40 import androidx.lifecycle.LiveData; 41 import androidx.lifecycle.ViewModelProvider; 42 43 import com.android.car.carlauncher.R; 44 import com.android.car.media.common.MediaItemMetadata; 45 import com.android.car.media.common.playback.PlaybackViewModel; 46 47 import java.util.ArrayList; 48 import java.util.List; 49 50 public final class CalmModeFragment extends Fragment { 51 private static final String TAG = CalmModeFragment.class.getSimpleName(); 52 private static final boolean DEBUG = Build.isDebuggable(); 53 private View mContainerView; 54 private TextView mMediaTitleView; 55 private TextView mNavStateView; 56 private TextView mTemperatureView; 57 private TextView mClockView; 58 private TextView mDateView; 59 private ImageView mNavStateIconView; 60 private TextView mTemperatureIconView; 61 private ViewModelProvider mViewModelProvider; 62 private TemperatureViewModel mTemperatureViewModel; 63 private LiveData<TemperatureData> mTemperatureData; 64 private PlaybackViewModel mPlaybackViewModel; 65 @Nullable 66 private NavigationStateViewModel mNavigationStateViewModel; 67 private boolean mShowEntryAnimations; 68 private final List<View> mViewsPendingAnimation = new ArrayList<>(); 69 private final AnimatorSet mAnimatorSet = new AnimatorSet(); 70 71 @Override onCreateView(@onNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)72 public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, 73 @Nullable Bundle savedInstanceState) { 74 if (DEBUG) { 75 Log.v(TAG, "onCreateView"); 76 } 77 78 View rootView = inflater.inflate(R.layout.calm_mode_fragment, container, false); 79 mViewModelProvider = new ViewModelProvider(getViewModelStore(), 80 new ViewModelProvider.AndroidViewModelFactory(requireActivity().getApplication())); 81 82 mContainerView = rootView.findViewById(R.id.calm_mode_container); 83 mClockView = rootView.findViewById(R.id.clock); 84 mDateView = rootView.findViewById(R.id.date); 85 mNavStateView = rootView.findViewById(R.id.nav_state); 86 mNavStateIconView = rootView.findViewById(R.id.nav_state_icon); 87 mTemperatureIconView = rootView.findViewById(R.id.temperature_icon); 88 mTemperatureView = rootView.findViewById(R.id.temperature); 89 mMediaTitleView = rootView.findViewById(R.id.media_title); 90 mShowEntryAnimations = true; 91 92 hideStatusBar(); 93 initExitOnClick(); 94 initDate(); 95 initNavState(); 96 initTemperature(); 97 initMediaTitle(); 98 initClockAndPlayEntryAnimations(); 99 100 return rootView; 101 } 102 hideStatusBar()103 private void hideStatusBar() { 104 final Handler handler = new Handler(Looper.getMainLooper()); 105 handler.postDelayed( 106 () -> { 107 if (getActivity() == null) return; 108 getActivity().getWindow().getDecorView().getWindowInsetsController() 109 .hide(WindowInsets.Type.statusBars()); 110 }, 111 getResources().getInteger(R.integer.calm_mode_activity_fade_duration)); 112 } 113 initMediaTitle()114 private void initMediaTitle() { 115 if (DEBUG) { 116 Log.v(TAG, "initMediaTitle()"); 117 } 118 if (shouldShowMedia()) { 119 mPlaybackViewModel = PlaybackViewModel.get(requireActivity().getApplication(), 120 MEDIA_SOURCE_MODE_PLAYBACK); 121 mPlaybackViewModel.getMetadata().observe(this, this::updateMediaTitle); 122 } 123 } 124 initTemperature()125 private void initTemperature() { 126 if (DEBUG) { 127 Log.v(TAG, "initTemperature()"); 128 } 129 if (shouldShowTemperature()) { 130 mTemperatureViewModel = mViewModelProvider.get(TemperatureViewModel.class); 131 mTemperatureData = mTemperatureViewModel.getTemperatureData(); 132 mTemperatureData.observe(getViewLifecycleOwner(), this::updateTemperatureData); 133 } 134 } 135 initDate()136 private void initDate() { 137 if (DEBUG) { 138 Log.v(TAG, "initDate()"); 139 } 140 if (!shouldShowDate()) { 141 return; 142 } 143 if (mShowEntryAnimations) { 144 mDateView.setVisibility(View.INVISIBLE); 145 mViewsPendingAnimation.add(mDateView); 146 } else { 147 mDateView.setVisibility(View.VISIBLE); 148 } 149 150 } 151 playEntryAnimations()152 private void playEntryAnimations() { 153 mShowEntryAnimations = false; 154 List<Animator> animList = new ArrayList<>(); 155 156 for (View view : mViewsPendingAnimation) { 157 if (getContext() == null) { 158 return; 159 } 160 Animator animator = AnimatorInflater.loadAnimator(getContext(), 161 R.animator.calm_mode_enter); 162 animator.setTarget(view); 163 Animator.AnimatorListener animatorListener = new Animator.AnimatorListener() { 164 @Override 165 public void onAnimationStart(@NonNull Animator animation) { 166 view.setVisibility(View.VISIBLE); 167 } 168 169 @Override 170 public void onAnimationEnd(@NonNull Animator animation) { 171 172 } 173 174 @Override 175 public void onAnimationCancel(@NonNull Animator animation) { 176 177 } 178 179 @Override 180 public void onAnimationRepeat(@NonNull Animator animation) { 181 182 } 183 }; 184 animator.addListener(animatorListener); 185 animList.add(animator); 186 } 187 mViewsPendingAnimation.clear(); 188 mAnimatorSet.setDuration(getResources().getInteger( 189 R.integer.calm_mode_content_fade_duration)); 190 mAnimatorSet.playTogether(animList); 191 mAnimatorSet.start(); 192 } initClockAndPlayEntryAnimations()193 private void initClockAndPlayEntryAnimations() { 194 if (DEBUG) { 195 Log.v(TAG, "initClock()"); 196 } 197 if (shouldShowClock()) { 198 mClockView.setVisibility(View.INVISIBLE); 199 if (getContext() == null) { 200 return; 201 } 202 Animator animator = AnimatorInflater.loadAnimator(getContext(), 203 R.animator.calm_mode_enter); 204 animator.setStartDelay(getResources().getInteger( 205 R.integer.calm_mode_activity_fade_duration)); 206 animator.setTarget(mClockView); 207 208 animator.addListener(new Animator.AnimatorListener() { 209 @Override 210 public void onAnimationStart(@NonNull Animator animation) { 211 mClockView.setVisibility(View.VISIBLE); 212 } 213 214 @Override 215 public void onAnimationEnd(@NonNull Animator animation) { 216 playEntryAnimations(); 217 } 218 219 @Override 220 public void onAnimationCancel(@NonNull Animator animation) { 221 222 } 223 224 @Override 225 public void onAnimationRepeat(@NonNull Animator animation) { 226 227 } 228 }); 229 animator.start(); 230 } 231 } 232 initExitOnClick()233 private void initExitOnClick() { 234 if (DEBUG) { 235 Log.v(TAG, "initExitOnTouch()"); 236 } 237 mContainerView.setOnClickListener((view) -> { 238 if (DEBUG) { 239 Log.v(TAG, "Detected touch, exiting Calm mode"); 240 } 241 if (getActivity() != null) { 242 getActivity().finish(); 243 } 244 }); 245 } 246 initNavState()247 private void initNavState() { 248 if (DEBUG) { 249 Log.v(TAG, "initNavState()"); 250 } 251 252 if (shouldShowNavigation()) { 253 mNavigationStateViewModel = mViewModelProvider.get(NavigationStateViewModel.class); 254 mNavigationStateViewModel 255 .getNavigationState() 256 .observe(this, this::updateNavigationState); 257 } 258 } 259 shouldShowMedia()260 private boolean shouldShowMedia() { 261 return getResources().getBoolean(R.bool.config_calmMode_showMedia); 262 } 263 shouldShowTemperature()264 private boolean shouldShowTemperature() { 265 return getResources().getBoolean(R.bool.config_calmMode_showTemperature); 266 } 267 shouldShowClock()268 private boolean shouldShowClock() { 269 return getResources().getBoolean(R.bool.config_calmMode_showClock); 270 } 271 shouldShowDate()272 private boolean shouldShowDate() { 273 return getResources().getBoolean(R.bool.config_calmMode_showDate); 274 } 275 shouldShowNavigation()276 private boolean shouldShowNavigation() { 277 return getResources().getBoolean(R.bool.config_calmMode_showNavigation); 278 } 279 updateTemperatureData(@ullable TemperatureData temperatureData)280 private void updateTemperatureData(@Nullable TemperatureData temperatureData) { 281 if (temperatureData == null) { 282 mTemperatureView.setVisibility(View.GONE); 283 mTemperatureIconView.setVisibility(View.GONE); 284 mTemperatureView.setText(""); 285 return; 286 } 287 if (mShowEntryAnimations) { 288 mTemperatureView.setVisibility(View.INVISIBLE); 289 mTemperatureIconView.setVisibility(View.INVISIBLE); 290 mViewsPendingAnimation.add(mTemperatureView); 291 mViewsPendingAnimation.add(mTemperatureIconView); 292 } else { 293 mTemperatureView.setVisibility(View.VISIBLE); 294 mTemperatureIconView.setVisibility(View.VISIBLE); 295 } 296 mTemperatureView.setText( 297 TemperatureData.buildTemperatureString( 298 temperatureData, getResources().getConfiguration().getLocales().get(0), 299 /* showUnit = */ false)); 300 } 301 updateNavigationState(NavigationStateData navState)302 private void updateNavigationState(NavigationStateData navState) { 303 if (DEBUG) { 304 Log.v(TAG, "updateNavigationState navState = " + navState); 305 } 306 307 if (navState == null) { 308 mNavStateView.setVisibility(View.GONE); 309 mNavStateIconView.setVisibility(View.GONE); 310 mNavStateView.setText(""); 311 return; 312 } 313 314 if (mShowEntryAnimations) { 315 mNavStateView.setVisibility(View.INVISIBLE); 316 mNavStateIconView.setVisibility(View.INVISIBLE); 317 mViewsPendingAnimation.add(mNavStateView); 318 mViewsPendingAnimation.add(mNavStateIconView); 319 } else { 320 mNavStateView.setVisibility(View.VISIBLE); 321 mNavStateIconView.setVisibility(View.VISIBLE); 322 } 323 mNavStateView.setText( 324 NavigationStateData.buildTripStatusString(navState, 325 getResources().getConfiguration().getLocales().get(0), 326 getResources().getString(R.string.calm_mode_separator))); 327 } 328 329 @VisibleForTesting updateMediaTitle(MediaItemMetadata mediaItemMetadata)330 void updateMediaTitle(MediaItemMetadata mediaItemMetadata) { 331 if (DEBUG) { 332 Log.v(TAG, "updateMediaTitle mediaItemMetadata = " + mediaItemMetadata); 333 } 334 if (mediaItemMetadata == null || mediaItemMetadata.getTitle() == null 335 || mediaItemMetadata.getTitle().length() == 0) { 336 mMediaTitleView.setVisibility(View.GONE); 337 mClockView.setTranslationY(0f); 338 mMediaTitleView.setText(""); 339 return; 340 } 341 342 StringBuilder medaTitleBuilder = new StringBuilder(); 343 medaTitleBuilder.append(mediaItemMetadata.getTitle()); 344 345 if (mediaItemMetadata.getSubtitle() != null 346 && mediaItemMetadata.getSubtitle().length() > 0) { 347 medaTitleBuilder.append(getResources().getString(R.string.calm_mode_separator)); 348 medaTitleBuilder.append(mediaItemMetadata.getSubtitle()); 349 } 350 351 mClockView.setTranslationY( 352 -getResources().getDimension(R.dimen.calm_mode_clock_translationY)); 353 mContainerView.requestLayout(); 354 if (mShowEntryAnimations) { 355 mMediaTitleView.setVisibility(View.INVISIBLE); 356 mViewsPendingAnimation.add(mMediaTitleView); 357 } else { 358 mMediaTitleView.setVisibility(View.VISIBLE); 359 } 360 mMediaTitleView.setText(medaTitleBuilder.toString()); 361 } 362 363 @Override onStart()364 public void onStart() { 365 super.onStart(); 366 int launchType = requireActivity().getIntent().getIntExtra( 367 CalmModeStatsLogHelper.INTENT_EXTRA_CALM_MODE_LAUNCH_TYPE, 368 CalmModeStatsLogHelper.CalmModeLaunchType.UNSPECIFIED_LAUNCH_TYPE); 369 CalmModeStatsLogHelper.getInstance().logSessionStarted(launchType); 370 } 371 372 @Override onStop()373 public void onStop() { 374 super.onStop(); 375 CalmModeStatsLogHelper.getInstance().logSessionFinished(); 376 } 377 378 } 379