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.settings.applications.appcompat;
18 
19 import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
20 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
21 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_16_9;
22 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
23 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_4_3;
24 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_APP_DEFAULT;
25 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_DISPLAY_SIZE;
26 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN;
27 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN;
28 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET;
29 
30 import android.app.ActivityManager;
31 import android.app.IActivityManager;
32 import android.app.settings.SettingsEnums;
33 import android.content.Intent;
34 import android.content.pm.PackageManager;
35 import android.os.Build;
36 import android.os.Bundle;
37 import android.os.RemoteException;
38 import android.os.UserHandle;
39 import android.util.Log;
40 
41 import androidx.annotation.NonNull;
42 import androidx.appcompat.app.AlertDialog;
43 import androidx.preference.Preference;
44 
45 import com.android.internal.annotations.VisibleForTesting;
46 import com.android.settings.R;
47 import com.android.settings.Utils;
48 import com.android.settings.applications.AppInfoBase;
49 import com.android.settings.overlay.FeatureFactory;
50 import com.android.settings.widget.EntityHeaderController;
51 import com.android.settingslib.applications.AppUtils;
52 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
53 import com.android.settingslib.widget.ActionButtonsPreference;
54 
55 import com.google.common.collect.BiMap;
56 import com.google.common.collect.HashBiMap;
57 
58 import java.util.ArrayList;
59 import java.util.List;
60 
61 /**
62  * App specific activity to show aspect ratio overrides
63  */
64 public class UserAspectRatioDetails extends AppInfoBase implements
65         RadioWithImagePreference.OnClickListener {
66     private static final String TAG = UserAspectRatioDetails.class.getSimpleName();
67 
68     private static final String KEY_HEADER_SUMMARY = "app_aspect_ratio_summary";
69     @VisibleForTesting
70     static final String KEY_HEADER_BUTTONS = "header_view";
71 
72     private static final String KEY_PREF_HALF_SCREEN = "half_screen_pref";
73     private static final String KEY_PREF_DISPLAY_SIZE = "display_size_pref";
74     private static final String KEY_PREF_16_9 = "16_9_pref";
75     private static final String KEY_PREF_4_3 = "4_3_pref";
76     @VisibleForTesting
77     static final String KEY_PREF_FULLSCREEN = "fullscreen_pref";
78     @VisibleForTesting
79     static final String KEY_PREF_DEFAULT = "app_default_pref";
80     @VisibleForTesting
81     static final String KEY_PREF_3_2 = "3_2_pref";
82 
83     @VisibleForTesting
84     @NonNull String mSelectedKey = KEY_PREF_DEFAULT;
85 
86     /** Radio button preference key mapped to {@link PackageManager.UserMinAspectRatio} value */
87     @VisibleForTesting
88     final BiMap<String, Integer> mKeyToAspectRatioMap = HashBiMap.create();
89 
90     private final List<RadioWithImagePreference> mAspectRatioPreferences = new ArrayList<>();
91 
92     @NonNull private UserAspectRatioManager mUserAspectRatioManager;
93     private boolean mIsOverrideToFullscreenEnabled;
94 
95     @Override
onCreate(@onNull Bundle savedInstanceState)96     public void onCreate(@NonNull Bundle savedInstanceState) {
97         super.onCreate(savedInstanceState);
98 
99         mUserAspectRatioManager = new UserAspectRatioManager(getContext());
100         mIsOverrideToFullscreenEnabled = getAspectRatioManager()
101                 .isOverrideToFullscreenEnabled(mPackageName, mUserId);
102 
103         initPreferences();
104         try {
105             final int userAspectRatio = getAspectRatioManager()
106                     .getUserMinAspectRatioValue(mPackageName, mUserId);
107             mSelectedKey = getSelectedKey(userAspectRatio);
108         } catch (RemoteException e) {
109             Log.e(TAG, "Unable to get user min aspect ratio");
110         }
111         refreshUi();
112     }
113 
114     @Override
onRadioButtonClicked(@onNull RadioWithImagePreference selected)115     public void onRadioButtonClicked(@NonNull RadioWithImagePreference selected) {
116         final String selectedKey = selected.getKey();
117         if (mSelectedKey.equals(selectedKey)) {
118             return;
119         }
120         final int userAspectRatio = getSelectedUserMinAspectRatio(selectedKey);
121         try {
122             getAspectRatioManager().setUserMinAspectRatio(mPackageName, mUserId, userAspectRatio);
123         } catch (RemoteException e) {
124             Log.e(TAG, "Unable to set user min aspect ratio");
125             return;
126         }
127         logActionMetrics(selectedKey, mSelectedKey);
128         // Only update to selected aspect ratio if nothing goes wrong
129         mSelectedKey = selectedKey;
130         updateAllPreferences(mSelectedKey);
131         Log.d(TAG, "Killing application process " + mPackageName);
132         try {
133             final IActivityManager am = ActivityManager.getService();
134             am.stopAppForUser(mPackageName, mUserId);
135         } catch (RemoteException e) {
136             Log.e(TAG, "Unable to stop application " + mPackageName);
137         }
138     }
139 
140     @Override
getMetricsCategory()141     public int getMetricsCategory() {
142         return SettingsEnums.USER_ASPECT_RATIO_APP_INFO_SETTINGS;
143     }
144 
145     @Override
refreshUi()146     protected boolean refreshUi() {
147         if (mPackageInfo == null || mPackageInfo.applicationInfo == null) {
148             return false;
149         }
150         updateAllPreferences(mSelectedKey);
151         return true;
152     }
153 
154     @Override
createDialog(int id, int errorCode)155     protected AlertDialog createDialog(int id, int errorCode) {
156         return null;
157     }
158 
launchApplication()159     private void launchApplication() {
160         Intent launchIntent = mPm.getLaunchIntentForPackage(mPackageName)
161                 .addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP);
162         if (launchIntent != null) {
163             getContext().startActivityAsUser(launchIntent, new UserHandle(mUserId));
164         }
165     }
166 
167     @PackageManager.UserMinAspectRatio
168     @VisibleForTesting
getSelectedUserMinAspectRatio(@onNull String selectedKey)169     int getSelectedUserMinAspectRatio(@NonNull String selectedKey) {
170         final int appDefault = mKeyToAspectRatioMap
171                 .getOrDefault(KEY_PREF_DEFAULT, USER_MIN_ASPECT_RATIO_UNSET);
172         return mKeyToAspectRatioMap.getOrDefault(selectedKey, appDefault);
173     }
174 
175     @NonNull
getSelectedKey(@ackageManager.UserMinAspectRatio int userMinAspectRatio)176     private String getSelectedKey(@PackageManager.UserMinAspectRatio int userMinAspectRatio) {
177         final String appDefault = mKeyToAspectRatioMap.inverse()
178                 .getOrDefault(USER_MIN_ASPECT_RATIO_UNSET, KEY_PREF_DEFAULT);
179 
180         if (userMinAspectRatio == USER_MIN_ASPECT_RATIO_UNSET && mIsOverrideToFullscreenEnabled) {
181             // Pre-select fullscreen option if device manufacturer has overridden app to fullscreen
182             userMinAspectRatio = USER_MIN_ASPECT_RATIO_FULLSCREEN;
183         }
184         return mKeyToAspectRatioMap.inverse().getOrDefault(userMinAspectRatio, appDefault);
185     }
186 
187     @Override
onActivityCreated(Bundle savedInstanceState)188     public void onActivityCreated(Bundle savedInstanceState) {
189         super.onActivityCreated(savedInstanceState);
190         final Preference pref = EntityHeaderController
191                 .newInstance(getActivity(), this, null /* header */)
192                 .setIcon(Utils.getBadgedIcon(getContext(), mPackageInfo.applicationInfo))
193                 .setLabel(mPackageInfo.applicationInfo.loadLabel(mPm))
194                 .setIsInstantApp(AppUtils.isInstant(mPackageInfo.applicationInfo))
195                 .setPackageName(mPackageName)
196                 .setUid(mPackageInfo.applicationInfo.uid)
197                 .setHasAppInfoLink(true)
198                 .setButtonActions(EntityHeaderController.ActionType.ACTION_NONE,
199                         EntityHeaderController.ActionType.ACTION_NONE)
200                 .done(getPrefContext());
201 
202         getPreferenceScreen().addPreference(pref);
203     }
204 
initPreferences()205     private void initPreferences() {
206         addPreferencesFromResource(R.xml.user_aspect_ratio_details);
207 
208         final String summary = getContext().getResources().getString(
209                 R.string.aspect_ratio_main_summary, Build.MODEL);
210         findPreference(KEY_HEADER_SUMMARY).setTitle(summary);
211 
212         ((ActionButtonsPreference) findPreference(KEY_HEADER_BUTTONS))
213                 .setButton1Text(R.string.launch_instant_app)
214                 .setButton1Icon(R.drawable.ic_settings_open)
215                 .setButton1OnClickListener(v -> launchApplication());
216 
217         if (mIsOverrideToFullscreenEnabled) {
218             addPreference(KEY_PREF_DEFAULT, USER_MIN_ASPECT_RATIO_APP_DEFAULT);
219         } else {
220             addPreference(KEY_PREF_DEFAULT, USER_MIN_ASPECT_RATIO_UNSET);
221         }
222         addPreference(KEY_PREF_FULLSCREEN, USER_MIN_ASPECT_RATIO_FULLSCREEN);
223         addPreference(KEY_PREF_DISPLAY_SIZE, USER_MIN_ASPECT_RATIO_DISPLAY_SIZE);
224         addPreference(KEY_PREF_HALF_SCREEN, USER_MIN_ASPECT_RATIO_SPLIT_SCREEN);
225         addPreference(KEY_PREF_16_9, USER_MIN_ASPECT_RATIO_16_9);
226         addPreference(KEY_PREF_4_3, USER_MIN_ASPECT_RATIO_4_3);
227         addPreference(KEY_PREF_3_2, USER_MIN_ASPECT_RATIO_3_2);
228     }
229 
addPreference(@onNull String key, @PackageManager.UserMinAspectRatio int aspectRatio)230     private void addPreference(@NonNull String key,
231             @PackageManager.UserMinAspectRatio int aspectRatio) {
232         final RadioWithImagePreference pref = findPreference(key);
233         if (pref == null) {
234             return;
235         }
236         if (!getAspectRatioManager().hasAspectRatioOption(aspectRatio, mPackageName)) {
237             pref.setVisible(false);
238             return;
239         }
240         pref.setTitle(mUserAspectRatioManager.getAccessibleEntry(aspectRatio, mPackageName));
241         pref.setOrder(getAspectRatioManager().getUserMinAspectRatioOrder(aspectRatio));
242         pref.setOnClickListener(this);
243         mKeyToAspectRatioMap.put(key, aspectRatio);
244         mAspectRatioPreferences.add(pref);
245     }
246 
updateAllPreferences(@onNull String selectedKey)247     private void updateAllPreferences(@NonNull String selectedKey) {
248         for (RadioWithImagePreference pref : mAspectRatioPreferences) {
249             pref.setChecked(selectedKey.equals(pref.getKey()));
250         }
251     }
252 
logActionMetrics(@onNull String selectedKey, @NonNull String unselectedKey)253     private void logActionMetrics(@NonNull String selectedKey, @NonNull String unselectedKey) {
254         final MetricsFeatureProvider metricsFeatureProvider =
255                 FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
256         final int attribution = metricsFeatureProvider.getAttribution(getActivity());
257         metricsFeatureProvider.action(
258                 attribution,
259                 getUnselectedAspectRatioAction(unselectedKey),
260                 getMetricsCategory(),
261                 mPackageName,
262                 mUserId
263         );
264         metricsFeatureProvider.action(
265                 attribution,
266                 getSelectedAspectRatioAction(selectedKey),
267                 getMetricsCategory(),
268                 mPackageName,
269                 mUserId
270         );
271     }
272 
getSelectedAspectRatioAction(@onNull String selectedKey)273     private static int getSelectedAspectRatioAction(@NonNull String selectedKey) {
274         switch (selectedKey) {
275             case KEY_PREF_DEFAULT:
276                 return SettingsEnums.ACTION_USER_ASPECT_RATIO_APP_DEFAULT_SELECTED;
277             case KEY_PREF_FULLSCREEN:
278                 return SettingsEnums.ACTION_USER_ASPECT_RATIO_FULL_SCREEN_SELECTED;
279             case KEY_PREF_HALF_SCREEN:
280                 return SettingsEnums.ACTION_USER_ASPECT_RATIO_HALF_SCREEN_SELECTED;
281             case KEY_PREF_4_3:
282                 return SettingsEnums.ACTION_USER_ASPECT_RATIO_4_3_SELECTED;
283             case KEY_PREF_16_9:
284                 return SettingsEnums.ACTION_USER_ASPECT_RATIO_16_9_SELECTED;
285             case KEY_PREF_3_2:
286                 return SettingsEnums.ACTION_USER_ASPECT_RATIO_3_2_SELECTED;
287             case KEY_PREF_DISPLAY_SIZE:
288                 return SettingsEnums.ACTION_USER_ASPECT_RATIO_DISPLAY_SIZE_SELECTED;
289             default:
290                 return SettingsEnums.ACTION_UNKNOWN;
291         }
292     }
293 
getUnselectedAspectRatioAction(@onNull String unselectedKey)294     private static int getUnselectedAspectRatioAction(@NonNull String unselectedKey) {
295         switch (unselectedKey) {
296             case KEY_PREF_DEFAULT:
297                 return SettingsEnums.ACTION_USER_ASPECT_RATIO_APP_DEFAULT_UNSELECTED;
298             case KEY_PREF_FULLSCREEN:
299                 return SettingsEnums.ACTION_USER_ASPECT_RATIO_FULL_SCREEN_UNSELECTED;
300             case KEY_PREF_HALF_SCREEN:
301                 return SettingsEnums.ACTION_USER_ASPECT_RATIO_HALF_SCREEN_UNSELECTED;
302             case KEY_PREF_4_3:
303                 return SettingsEnums.ACTION_USER_ASPECT_RATIO_4_3_UNSELECTED;
304             case KEY_PREF_16_9:
305                 return SettingsEnums.ACTION_USER_ASPECT_RATIO_16_9_UNSELECTED;
306             case KEY_PREF_3_2:
307                 return SettingsEnums.ACTION_USER_ASPECT_RATIO_3_2_UNSELECTED;
308             case KEY_PREF_DISPLAY_SIZE:
309                 return SettingsEnums.ACTION_USER_ASPECT_RATIO_DISPLAY_SIZE_UNSELECTED;
310             default:
311                 return SettingsEnums.ACTION_UNKNOWN;
312         }
313     }
314 
315     @VisibleForTesting
getAspectRatioManager()316     UserAspectRatioManager getAspectRatioManager() {
317         return mUserAspectRatioManager;
318     }
319 }
320