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