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.google.android.car.kitchensink.input;
18 
19 import static android.car.Car.CAR_OCCUPANT_ZONE_SERVICE;
20 import static android.car.Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER;
21 import static android.car.CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER;
22 
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.car.Car;
26 import android.car.CarOccupantZoneManager;
27 import android.car.settings.CarSettings;
28 import android.content.ContentResolver;
29 import android.database.ContentObserver;
30 import android.hardware.display.DisplayManager;
31 import android.net.Uri;
32 import android.os.Bundle;
33 import android.os.Handler;
34 import android.os.Looper;
35 import android.provider.Settings;
36 import android.text.TextUtils;
37 import android.util.ArraySet;
38 import android.util.Log;
39 import android.view.Display;
40 import android.view.LayoutInflater;
41 import android.view.View;
42 import android.view.ViewGroup;
43 import android.widget.ListView;
44 
45 import androidx.fragment.app.Fragment;
46 
47 import com.google.android.car.kitchensink.R;
48 
49 import java.util.ArrayList;
50 import java.util.List;
51 
52 public final class DisplayInputLockTestFragment extends Fragment {
53     private static final String TAG = "DisplayInputLock";
54     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
55 
56     private Car mCar;
57     private CarOccupantZoneManager mOccupantZoneManager;
58     private DisplayManager mDisplayManager;
59 
60     // Array of display unique ids from the display input lock setting.
61     private final ArraySet<String> mDisplayInputLockSetting = new ArraySet<>();
62 
63     private final ArrayList<DisplayInputLockItem> mDisplayInputLockItems = new ArrayList<>();
64     private DisplayInputLockListAdapter mDisplayInputLockListAdapter;
65 
66     private final DisplayManager.DisplayListener mDisplayListener =
67             new DisplayManager.DisplayListener() {
68         @Override
69         public void onDisplayAdded(int displayId) {
70             if (DEBUG) {
71                 Log.d(TAG, "onDisplayAdded: display " + displayId);
72             }
73             Display display = mDisplayManager.getDisplay(displayId);
74             mDisplayInputLockItems.add(new DisplayInputLockItem(displayId,
75                         mDisplayInputLockSetting.contains(display.getUniqueId())));
76             mDisplayInputLockListAdapter.setListItems(mDisplayInputLockItems);
77         }
78 
79         @Override
80         public void onDisplayRemoved(int displayId) {
81             if (DEBUG) {
82                 Log.d(TAG, "onDisplayRemoved: display " + displayId);
83             }
84             mDisplayInputLockItems.removeIf(item -> item.mDisplayId == displayId);
85             mDisplayInputLockListAdapter.setListItems(mDisplayInputLockItems);
86         }
87 
88         @Override
89         public void onDisplayChanged(int displayId) {
90         }
91     };
92 
93     static class DisplayInputLockItem {
94         public final int mDisplayId;
95         public boolean mIsLockEnabled;
96 
DisplayInputLockItem(int displayId, boolean isLockEnabled)97         DisplayInputLockItem(int displayId, boolean isLockEnabled) {
98             mDisplayId = displayId;
99             mIsLockEnabled = isLockEnabled;
100         }
101     }
102 
103     @Override
onCreate(Bundle savedInstanceState)104     public void onCreate(Bundle savedInstanceState) {
105         super.onCreate(savedInstanceState);
106         mDisplayManager = getContext().getSystemService(DisplayManager.class);
107     }
108 
109     @Nullable
110     @Override
onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)111     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
112             @Nullable Bundle savedInstanceState) {
113         View view = inflater.inflate(R.layout.display_input_lock_test_fragment, container,
114                 /* root= */ false);
115         connectCar();
116 
117         ListView inputLockListView = view.findViewById(R.id.display_input_lock_list);
118         initDisplayInputLockData();
119         mDisplayInputLockListAdapter = new DisplayInputLockListAdapter(getContext(),
120                 mDisplayInputLockItems, this);
121         inputLockListView.setAdapter(mDisplayInputLockListAdapter);
122         mDisplayManager.registerDisplayListener(mDisplayListener, /* handler= */ null);
123         Uri uri = Settings.Global.getUriFor(CarSettings.Global.DISPLAY_INPUT_LOCK);
124         getContext().getContentResolver()
125                 .registerContentObserver(uri, /* notifyForDescendants= */ false, mSettingObserver);
126 
127         return view;
128     }
129 
130     @Override
onDestroyView()131     public void onDestroyView() {
132         getContext().getContentResolver().unregisterContentObserver(mSettingObserver);
133         mDisplayManager.unregisterDisplayListener(mDisplayListener);
134         if (mCar != null && mCar.isConnected()) {
135             mCar.disconnect();
136             mCar = null;
137         }
138         super.onDestroyView();
139     }
140 
141     /**
142      * Requests update to the display input lock setting value.
143      *
144      * @param displayId The display for which the input lock is updated.
145      * @param enabled Whether to enable the display input lock.
146      */
requestUpdateDisplayInputLockSetting(int displayId, boolean enabled)147     public void requestUpdateDisplayInputLockSetting(int displayId, boolean enabled) {
148         String displayUniqueId = getDisplayUniqueId(displayId);
149         if (mDisplayInputLockSetting.contains(displayUniqueId) == enabled) {
150             return;
151         }
152         if (DEBUG) {
153             Log.d(TAG, "requestUpdateDisplayInputLockSetting: displayId=" + displayId
154                     + ", enabled=" + enabled);
155         }
156         if (enabled) {
157             mDisplayInputLockSetting.add(displayUniqueId);
158         } else {
159             mDisplayInputLockSetting.remove(displayUniqueId);
160         }
161         mDisplayInputLockItems.stream().filter(item -> item.mDisplayId == displayId)
162                 .findAny().ifPresent(item -> item.mIsLockEnabled = enabled);
163         writeDisplayInputLockSetting(getContext().getContentResolver(),
164                 CarSettings.Global.DISPLAY_INPUT_LOCK,
165                 makeDisplayInputLockSetting(mDisplayInputLockSetting));
166     }
167 
168     @Nullable
getDisplayInputLockSetting(@onNull ContentResolver resolver)169     private String getDisplayInputLockSetting(@NonNull ContentResolver resolver) {
170         return Settings.Global.getString(resolver,
171                 CarSettings.Global.DISPLAY_INPUT_LOCK);
172     }
173 
writeDisplayInputLockSetting(@onNull ContentResolver resolver, @NonNull String settingKey, @NonNull String value)174     private void writeDisplayInputLockSetting(@NonNull ContentResolver resolver,
175             @NonNull String settingKey, @NonNull String value) {
176         Settings.Global.putString(resolver, settingKey, value);
177     }
178 
makeDisplayInputLockSetting(@ullable ArraySet<String> inputLockSetting)179     private String makeDisplayInputLockSetting(@Nullable ArraySet<String> inputLockSetting) {
180         if (inputLockSetting == null) {
181             return "";
182         }
183 
184         String settingValue = TextUtils.join(",", inputLockSetting);
185         if (DEBUG) {
186             Log.d(TAG, "makeDisplayInputLockSetting(): add new input lock setting: "
187                     + settingValue);
188         }
189         return settingValue;
190     }
191 
getDisplayUniqueId(int displayId)192     private String getDisplayUniqueId(int displayId) {
193         Display display = mDisplayManager.getDisplay(displayId);
194         if (display == null) {
195             return "";
196         }
197         return display.getUniqueId();
198     }
199 
findDisplayIdByUniqueId(@onNull String displayUniqueId, Display[] displays)200     private int findDisplayIdByUniqueId(@NonNull String displayUniqueId, Display[] displays) {
201         for (int i = 0; i < displays.length; i++) {
202             Display display = displays[i];
203             if (displayUniqueId.equals(display.getUniqueId())) {
204                 return display.getDisplayId();
205             }
206         }
207         return Display.INVALID_DISPLAY;
208     }
209 
parseDisplayInputLockSettingValue(@onNull String settingKey, @Nullable String value)210     private void parseDisplayInputLockSettingValue(@NonNull String settingKey,
211             @Nullable String value) {
212         mDisplayInputLockSetting.clear();
213         if (value == null || value.isEmpty()) {
214             return;
215         }
216 
217         Display[] displays = mDisplayManager.getDisplays();
218         String[] entries = value.split(",");
219         for (String uniqueId : entries) {
220             if (findDisplayIdByUniqueId(uniqueId, displays) == Display.INVALID_DISPLAY) {
221                 Log.w(TAG, "Invalid display id: " + uniqueId);
222                 continue;
223             }
224             mDisplayInputLockSetting.add(uniqueId);
225         }
226     }
227 
initDisplayInputLockData()228     private void initDisplayInputLockData() {
229         // Read a setting value from the global setting of rear seat input lock.
230         String settingValue = getDisplayInputLockSetting(getContext().getContentResolver());
231         parseDisplayInputLockSettingValue(CarSettings.Global.DISPLAY_INPUT_LOCK, settingValue);
232 
233         List<CarOccupantZoneManager.OccupantZoneInfo> zonelist =
234                 mOccupantZoneManager.getAllOccupantZones();
235         // Make input lock list items for passenger displays with the setting value.
236         mDisplayInputLockItems.clear();
237         for (CarOccupantZoneManager.OccupantZoneInfo zone : zonelist) {
238             if (zone.occupantType == OCCUPANT_TYPE_DRIVER) {
239                 continue;
240             }
241 
242             Display display = mOccupantZoneManager.getDisplayForOccupant(zone,
243                     CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
244             if (display != null) {
245                 int displayId = display.getDisplayId();
246                 mDisplayInputLockItems.add(new DisplayInputLockItem(displayId,
247                         mDisplayInputLockSetting.contains(display.getUniqueId())));
248             }
249         }
250     }
251 
updateDisplayInputLockData()252     private boolean updateDisplayInputLockData() {
253         // Read a new setting value from the global setting of rear seat input lock.
254         String settingValue = getDisplayInputLockSetting(getContext().getContentResolver());
255         ArraySet<String> oldInputLockSetting = new ArraySet<>(mDisplayInputLockSetting);
256         parseDisplayInputLockSettingValue(CarSettings.Global.DISPLAY_INPUT_LOCK, settingValue);
257 
258         if (mDisplayInputLockSetting.equals(oldInputLockSetting)) {
259             // Input lock setting is same, no need to update UI.
260             if (DEBUG) {
261                 Log.d(TAG, "Input lock setting is same, no need to update UI.");
262             }
263             return false;
264         }
265 
266         // Update input lock items from the setting value.
267         for (DisplayInputLockItem item : mDisplayInputLockItems) {
268             item.mIsLockEnabled = mDisplayInputLockSetting.contains(
269                         getDisplayUniqueId(item.mDisplayId));
270         }
271 
272         return true;
273     }
274 
refreshDisplayInputLockList()275     private void refreshDisplayInputLockList() {
276         if (updateDisplayInputLockData()) {
277             mDisplayInputLockListAdapter.setListItems(mDisplayInputLockItems);
278         }
279     }
280 
281     private final ContentObserver mSettingObserver =
282             new ContentObserver(new Handler(Looper.getMainLooper())) {
283         @Override
284         public void onChange(boolean selfChange, Uri uri) {
285             super.onChange(selfChange, uri);
286             if (isResumed()) {
287                 if (DEBUG) {
288                     Log.d(TAG, "Content has changed for URI "  + uri);
289                 }
290                 refreshDisplayInputLockList();
291             }
292         }
293     };
294 
connectCar()295     private void connectCar() {
296         mCar = Car.createCar(getContext(), /* handler= */ null,
297                 CAR_WAIT_TIMEOUT_WAIT_FOREVER, (car, ready) -> {
298                     if (!ready) {
299                         return;
300                     }
301                     mOccupantZoneManager = (CarOccupantZoneManager)
302                             car.getCarManager(CAR_OCCUPANT_ZONE_SERVICE);
303                 });
304     }
305 }
306