1 /* 2 * Copyright (C) 2022 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.systemui.complication; 18 19 import static com.android.systemui.complication.dagger.ComplicationHostViewModule.SCOPED_COMPLICATIONS_LAYOUT; 20 import static com.android.systemui.complication.dagger.ComplicationModule.SCOPED_COMPLICATIONS_MODEL; 21 22 import android.graphics.Rect; 23 import android.graphics.Region; 24 import android.os.Debug; 25 import android.os.UserHandle; 26 import android.provider.Settings; 27 import android.util.Log; 28 import android.view.View; 29 30 import androidx.constraintlayout.widget.ConstraintLayout; 31 import androidx.lifecycle.LifecycleOwner; 32 33 import com.android.internal.annotations.VisibleForTesting; 34 import com.android.systemui.dreams.DreamOverlayStateController; 35 import com.android.systemui.util.ViewController; 36 import com.android.systemui.util.settings.SecureSettings; 37 38 import java.util.Collection; 39 import java.util.HashMap; 40 import java.util.List; 41 import java.util.stream.Collectors; 42 43 import javax.inject.Inject; 44 import javax.inject.Named; 45 46 /** 47 * The {@link ComplicationHostViewController} is responsible for displaying complications within 48 * a given container. It monitors the available {@link Complication} instances from 49 * {@link com.android.systemui.dreams.DreamOverlayStateController} and inserts/removes them through 50 * a {@link ComplicationLayoutEngine}. 51 */ 52 public class ComplicationHostViewController extends ViewController<ConstraintLayout> { 53 private static final String TAG = "ComplicationHostVwCtrl"; 54 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 55 56 private final ComplicationLayoutEngine mLayoutEngine; 57 private final DreamOverlayStateController mDreamOverlayStateController; 58 private final LifecycleOwner mLifecycleOwner; 59 private final ComplicationCollectionViewModel mComplicationCollectionViewModel; 60 private final HashMap<ComplicationId, Complication.ViewHolder> mComplications = new HashMap<>(); 61 @VisibleForTesting 62 boolean mIsAnimationEnabled; 63 64 @Inject ComplicationHostViewController( @amedSCOPED_COMPLICATIONS_LAYOUT) ConstraintLayout view, ComplicationLayoutEngine layoutEngine, DreamOverlayStateController dreamOverlayStateController, LifecycleOwner lifecycleOwner, @Named(SCOPED_COMPLICATIONS_MODEL) ComplicationCollectionViewModel viewModel, SecureSettings secureSettings)65 protected ComplicationHostViewController( 66 @Named(SCOPED_COMPLICATIONS_LAYOUT) ConstraintLayout view, 67 ComplicationLayoutEngine layoutEngine, 68 DreamOverlayStateController dreamOverlayStateController, 69 LifecycleOwner lifecycleOwner, 70 @Named(SCOPED_COMPLICATIONS_MODEL) ComplicationCollectionViewModel viewModel, 71 SecureSettings secureSettings) { 72 super(view); 73 mLayoutEngine = layoutEngine; 74 mLifecycleOwner = lifecycleOwner; 75 mComplicationCollectionViewModel = viewModel; 76 mDreamOverlayStateController = dreamOverlayStateController; 77 78 // Whether animations are enabled. 79 mIsAnimationEnabled = secureSettings.getFloatForUser( 80 Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f, UserHandle.USER_CURRENT) != 0.0f; 81 } 82 83 @Override onInit()84 protected void onInit() { 85 super.onInit(); 86 mComplicationCollectionViewModel.getComplications().observe(mLifecycleOwner, 87 complicationViewModels -> updateComplications(complicationViewModels)); 88 } 89 90 /** 91 * Returns the region in display space occupied by complications. Touches in this region 92 * (composed of a collection of individual rectangular regions) should be directed to the 93 * complications rather than the region underneath. 94 */ getTouchRegions()95 public Region getTouchRegions() { 96 final Region region = new Region(); 97 final Rect rect = new Rect(); 98 final int childCount = mView.getChildCount(); 99 for (int i = 0; i < childCount; i++) { 100 View child = mView.getChildAt(i); 101 if (child.getGlobalVisibleRect(rect)) { 102 region.op(rect, Region.Op.UNION); 103 } 104 } 105 106 return region; 107 } 108 updateComplications(Collection<ComplicationViewModel> complications)109 private void updateComplications(Collection<ComplicationViewModel> complications) { 110 if (DEBUG) { 111 Log.d(TAG, "updateComplications called. Callers = " + Debug.getCallers(25)); 112 Log.d(TAG, " mComplications = " + mComplications.toString()); 113 Log.d(TAG, " complications = " + complications.toString()); 114 } 115 final Collection<ComplicationId> ids = complications.stream() 116 .map(complicationViewModel -> complicationViewModel.getId()) 117 .collect(Collectors.toSet()); 118 119 final Collection<ComplicationId> removedComplicationIds = 120 mComplications.keySet().stream() 121 .filter(complicationId -> !ids.contains(complicationId)) 122 .collect(Collectors.toSet()); 123 124 // Trim removed complications 125 removedComplicationIds.forEach(complicationId -> { 126 mLayoutEngine.removeComplication(complicationId); 127 mComplications.remove(complicationId); 128 }); 129 130 // Add new complications 131 final Collection<ComplicationViewModel> newComplications = complications 132 .stream() 133 .filter(complication -> !mComplications.containsKey(complication.getId())) 134 .collect(Collectors.toSet()); 135 136 newComplications 137 .forEach(complication -> { 138 final ComplicationId id = complication.getId(); 139 final Complication.ViewHolder viewHolder = complication.getComplication() 140 .createView(complication); 141 142 final View view = viewHolder.getView(); 143 144 if (view == null) { 145 Log.e(TAG, "invalid complication view. null view supplied by ViewHolder"); 146 return; 147 } 148 149 // Complications to be added before dream entry animations are finished are set 150 // to invisible and are animated in. 151 if (!mDreamOverlayStateController.areEntryAnimationsFinished() 152 && mIsAnimationEnabled) { 153 view.setVisibility(View.INVISIBLE); 154 } 155 mComplications.put(id, viewHolder); 156 if (view.getParent() != null) { 157 Log.e(TAG, "View for complication " 158 + complication.getComplication().getClass() 159 + " already has a parent. Make sure not to reuse complication " 160 + "views!"); 161 } 162 mLayoutEngine.addComplication(id, view, 163 viewHolder.getLayoutParams(), viewHolder.getCategory()); 164 }); 165 } 166 167 @Override onViewAttached()168 protected void onViewAttached() { 169 } 170 171 @Override onViewDetached()172 protected void onViewDetached() { 173 } 174 175 /** 176 * Exposes the associated {@link View}. Since this {@link View} is instantiated through dagger 177 * in the {@link ComplicationHostViewController} constructor, the 178 * {@link ComplicationHostViewController} is responsible for surfacing it so that it can be 179 * included in the parent view hierarchy. 180 */ getView()181 public View getView() { 182 return mView; 183 } 184 185 /** 186 * Gets an unordered list of all the views at a particular position. 187 */ getViewsAtPosition(@omplicationLayoutParams.Position int position)188 public List<View> getViewsAtPosition(@ComplicationLayoutParams.Position int position) { 189 return mLayoutEngine.getViewsAtPosition(position); 190 } 191 } 192