1 /*
2  * Copyright (C) 2014 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.camera.settings;
18 
19 import android.Manifest;
20 import android.app.ActionBar;
21 import android.app.Activity;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.pm.PackageManager;
25 import android.content.SharedPreferences;
26 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
27 import android.os.Bundle;
28 import android.preference.ListPreference;
29 import android.preference.Preference;
30 import android.preference.Preference.OnPreferenceClickListener;
31 import android.preference.PreferenceFragment;
32 import android.preference.PreferenceGroup;
33 import android.preference.PreferenceScreen;
34 import androidx.fragment.app.FragmentActivity;
35 import android.view.MenuItem;
36 
37 import com.android.camera.FatalErrorHandler;
38 import com.android.camera.FatalErrorHandlerImpl;
39 import com.android.camera.debug.Log;
40 import com.android.camera.device.CameraId;
41 import com.android.camera.one.OneCamera.Facing;
42 import com.android.camera.one.OneCameraAccessException;
43 import com.android.camera.one.OneCameraCharacteristics;
44 import com.android.camera.one.OneCameraException;
45 import com.android.camera.one.OneCameraManager;
46 import com.android.camera.one.OneCameraModule;
47 import com.android.camera.settings.PictureSizeLoader.PictureSizes;
48 import com.android.camera.settings.SettingsUtil.SelectedVideoQualities;
49 import com.android.camera.util.CameraSettingsActivityHelper;
50 import com.android.camera.util.GoogleHelpHelper;
51 import com.android.camera.util.Size;
52 import com.android.camera2.R;
53 import com.android.ex.camera2.portability.CameraAgentFactory;
54 import com.android.ex.camera2.portability.CameraDeviceInfo;
55 
56 import java.text.DecimalFormat;
57 import java.util.ArrayList;
58 import java.util.List;
59 
60 /**
61  * Provides the settings UI for the Camera app.
62  */
63 public class CameraSettingsActivity extends FragmentActivity {
64 
65     /**
66      * Used to denote a subsection of the preference tree to display in the
67      * Fragment. For instance, if 'Advanced' key is provided, the advanced
68      * preference section will be treated as the root for display. This is used
69      * to enable activity transitions between preference sections, and allows
70      * back/up stack to operate correctly.
71      */
72     public static final String PREF_SCREEN_EXTRA = "pref_screen_extra";
73     public static final String HIDE_ADVANCED_SCREEN = "hide_advanced";
74     private static final int PERMISSION_REQUEST_CODE = 1;
75     private OneCameraManager mOneCameraManager;
76 
77     @Override
onCreate(Bundle savedInstanceState)78     public void onCreate(Bundle savedInstanceState) {
79         super.onCreate(savedInstanceState);
80 
81         FatalErrorHandler fatalErrorHandler = new FatalErrorHandlerImpl(this);
82         boolean hideAdvancedScreen = false;
83 
84         try {
85             mOneCameraManager = OneCameraModule.provideOneCameraManager();
86         } catch (OneCameraException e) {
87             // Log error and continue. Modules requiring OneCamera should check
88             // and handle if null by showing error dialog or other treatment.
89             fatalErrorHandler.onGenericCameraAccessFailure();
90         }
91 
92         // Check if manual exposure is available, so we can decide whether to
93         // display Advanced screen.
94         try {
95             CameraId frontCameraId = mOneCameraManager.findFirstCameraFacing(Facing.FRONT);
96             CameraId backCameraId = mOneCameraManager.findFirstCameraFacing(Facing.BACK);
97 
98             // The exposure compensation is supported when both of the following conditions meet
99             //   - we have the valid camera, and
100             //   - the valid camera supports the exposure compensation
101             boolean isExposureCompensationSupportedByFrontCamera = (frontCameraId != null) &&
102                     (mOneCameraManager.getOneCameraCharacteristics(frontCameraId)
103                             .isExposureCompensationSupported());
104             boolean isExposureCompensationSupportedByBackCamera = (backCameraId != null) &&
105                     (mOneCameraManager.getOneCameraCharacteristics(backCameraId)
106                             .isExposureCompensationSupported());
107 
108             // Hides the option if neither front and back camera support exposure compensation.
109             if (!isExposureCompensationSupportedByFrontCamera &&
110                     !isExposureCompensationSupportedByBackCamera) {
111                 hideAdvancedScreen = true;
112             }
113         } catch (OneCameraAccessException e) {
114             fatalErrorHandler.onGenericCameraAccessFailure();
115         }
116 
117         ActionBar actionBar = getActionBar();
118         actionBar.setDisplayHomeAsUpEnabled(true);
119         actionBar.setTitle(R.string.mode_settings);
120 
121         String prefKey = getIntent().getStringExtra(PREF_SCREEN_EXTRA);
122         CameraSettingsFragment dialog = new CameraSettingsFragment();
123         Bundle bundle = new Bundle(1);
124         bundle.putString(PREF_SCREEN_EXTRA, prefKey);
125         bundle.putBoolean(HIDE_ADVANCED_SCREEN, hideAdvancedScreen);
126         dialog.setArguments(bundle);
127         getFragmentManager().beginTransaction().replace(android.R.id.content, dialog).commit();
128     }
129 
130     @Override
onMenuItemSelected(int featureId, MenuItem item)131     public boolean onMenuItemSelected(int featureId, MenuItem item) {
132         int itemId = item.getItemId();
133         if (itemId == android.R.id.home) {
134             finish();
135             return true;
136         }
137         return true;
138     }
139 
140     public static class CameraSettingsFragment extends PreferenceFragment implements
141             OnSharedPreferenceChangeListener {
142 
143         public static final String PREF_CATEGORY_RESOLUTION = "pref_category_resolution";
144         public static final String PREF_CATEGORY_ADVANCED = "pref_category_advanced";
145         public static final String PREF_LAUNCH_HELP = "pref_launch_help";
146         private static final Log.Tag TAG = new Log.Tag("SettingsFragment");
147         private static DecimalFormat sMegaPixelFormat = new DecimalFormat("##0.0");
148         private String[] mCamcorderProfileNames;
149         private CameraDeviceInfo mInfos;
150         private String mPrefKey;
151         private boolean mHideAdvancedScreen;
152         private boolean mGetSubPrefAsRoot = true;
153 
154         // Selected resolutions for the different cameras and sizes.
155         private PictureSizes mPictureSizes;
156 
157         @Override
onCreate(Bundle savedInstanceState)158         public void onCreate(Bundle savedInstanceState) {
159             super.onCreate(savedInstanceState);
160             Bundle arguments = getArguments();
161             if (arguments != null) {
162                 mPrefKey = arguments.getString(PREF_SCREEN_EXTRA);
163                 mHideAdvancedScreen = arguments.getBoolean(HIDE_ADVANCED_SCREEN);
164             }
165             Context context = this.getActivity().getApplicationContext();
166             addPreferencesFromResource(R.xml.camera_preferences);
167             PreferenceScreen advancedScreen =
168                     (PreferenceScreen) findPreference(PREF_CATEGORY_ADVANCED);
169 
170             // If manual exposure not enabled, hide the Advanced screen.
171             if (mHideAdvancedScreen) {
172                 PreferenceScreen root = (PreferenceScreen) findPreference("prefscreen_top");
173                 root.removePreference(advancedScreen);
174             }
175 
176             // Allow the Helper to edit the full preference hierarchy, not the
177             // sub tree we may show as root. See {@link #getPreferenceScreen()}.
178             mGetSubPrefAsRoot = false;
179             CameraSettingsActivityHelper.addAdditionalPreferences(this, context);
180             mGetSubPrefAsRoot = true;
181 
182             mCamcorderProfileNames = getResources().getStringArray(R.array.camcorder_profile_names);
183             mInfos = CameraAgentFactory
184                     .getAndroidCameraAgent(context, CameraAgentFactory.CameraApi.API_1)
185                     .getCameraDeviceInfo();
186             CameraAgentFactory.recycle(CameraAgentFactory.CameraApi.API_1);
187         }
188 
189         @Override
onResume()190         public void onResume() {
191             super.onResume();
192             final Activity activity = this.getActivity();
193 
194             // Load the camera sizes.
195             loadSizes();
196 
197             // Send loaded sizes to additional preferences.
198             CameraSettingsActivityHelper.onSizesLoaded(this, mPictureSizes.backCameraSizes,
199                     new ListPreferenceFiller() {
200                         @Override
201                         public void fill(List<Size> sizes, ListPreference preference) {
202                             setEntriesForSelection(sizes, preference);
203                         }
204                     });
205 
206             // Make sure to hide settings for cameras that don't exist on this
207             // device.
208             setVisibilities();
209 
210             // Put in the summaries for the currently set values.
211             final PreferenceScreen resolutionScreen =
212                     (PreferenceScreen) findPreference(PREF_CATEGORY_RESOLUTION);
213             fillEntriesAndSummaries(resolutionScreen);
214             setPreferenceScreenIntent(resolutionScreen);
215 
216             final PreferenceScreen advancedScreen =
217                     (PreferenceScreen) findPreference(PREF_CATEGORY_ADVANCED);
218 
219             if (!mHideAdvancedScreen) {
220                 setPreferenceScreenIntent(advancedScreen);
221             }
222 
223             Preference helpPref = findPreference(PREF_LAUNCH_HELP);
224             helpPref.setOnPreferenceClickListener(
225                     new OnPreferenceClickListener() {
226                         @Override
227                         public boolean onPreferenceClick(Preference preference) {
228                             new GoogleHelpHelper(activity).launchGoogleHelp();
229                             return true;
230                         }
231                     });
232             getPreferenceScreen().getSharedPreferences()
233                     .registerOnSharedPreferenceChangeListener(this);
234         }
235 
236         /**
237          * Configure home-as-up for sub-screens.
238          */
setPreferenceScreenIntent(final PreferenceScreen preferenceScreen)239         private void setPreferenceScreenIntent(final PreferenceScreen preferenceScreen) {
240             Intent intent = new Intent(getActivity(), CameraSettingsActivity.class);
241             intent.putExtra(PREF_SCREEN_EXTRA, preferenceScreen.getKey());
242             preferenceScreen.setIntent(intent);
243         }
244 
245         /**
246          * This override allows the CameraSettingsFragment to be reused for
247          * different nested PreferenceScreens within the single camera
248          * preferences XML resource. If the fragment is constructed with a
249          * desired preference key (delivered via an extra in the creation
250          * intent), it is used to look up the nested PreferenceScreen and
251          * returned here.
252          */
253         @Override
getPreferenceScreen()254         public PreferenceScreen getPreferenceScreen() {
255             PreferenceScreen root = super.getPreferenceScreen();
256             if (!mGetSubPrefAsRoot || mPrefKey == null || root == null) {
257                 return root;
258             } else {
259                 PreferenceScreen match = findByKey(root, mPrefKey);
260                 if (match != null) {
261                     return match;
262                 } else {
263                     throw new RuntimeException("key " + mPrefKey + " not found");
264                 }
265             }
266         }
267 
findByKey(PreferenceScreen parent, String key)268         private PreferenceScreen findByKey(PreferenceScreen parent, String key) {
269             if (key.equals(parent.getKey())) {
270                 return parent;
271             } else {
272                 for (int i = 0; i < parent.getPreferenceCount(); i++) {
273                     Preference child = parent.getPreference(i);
274                     if (child instanceof PreferenceScreen) {
275                         PreferenceScreen match = findByKey((PreferenceScreen) child, key);
276                         if (match != null) {
277                             return match;
278                         }
279                     }
280                 }
281                 return null;
282             }
283         }
284 
285         /**
286          * Depending on camera availability on the device, this removes settings
287          * for cameras the device doesn't have.
288          */
setVisibilities()289         private void setVisibilities() {
290             PreferenceGroup resolutions =
291                     (PreferenceGroup) findPreference(PREF_CATEGORY_RESOLUTION);
292             if (mPictureSizes.backCameraSizes.isEmpty()) {
293                 recursiveDelete(resolutions,
294                         findPreference(Keys.KEY_PICTURE_SIZE_BACK));
295                 recursiveDelete(resolutions,
296                         findPreference(Keys.KEY_VIDEO_QUALITY_BACK));
297             }
298             if (mPictureSizes.frontCameraSizes.isEmpty()) {
299                 recursiveDelete(resolutions,
300                         findPreference(Keys.KEY_PICTURE_SIZE_FRONT));
301                 recursiveDelete(resolutions,
302                         findPreference(Keys.KEY_VIDEO_QUALITY_FRONT));
303             }
304         }
305 
306         /**
307          * Recursively go through settings and fill entries and summaries of our
308          * preferences.
309          */
fillEntriesAndSummaries(PreferenceGroup group)310         private void fillEntriesAndSummaries(PreferenceGroup group) {
311             for (int i = 0; i < group.getPreferenceCount(); ++i) {
312                 Preference pref = group.getPreference(i);
313                 if (pref instanceof PreferenceGroup) {
314                     fillEntriesAndSummaries((PreferenceGroup) pref);
315                 }
316                 setSummary(pref);
317                 setEntries(pref);
318             }
319         }
320 
321         /**
322          * Recursively traverses the tree from the given group as the route and
323          * tries to delete the preference. Traversal stops once the preference
324          * was found and removed.
325          */
recursiveDelete(PreferenceGroup group, Preference preference)326         private boolean recursiveDelete(PreferenceGroup group, Preference preference) {
327             if (group == null) {
328                 Log.d(TAG, "attempting to delete from null preference group");
329                 return false;
330             }
331             if (preference == null) {
332                 Log.d(TAG, "attempting to delete null preference");
333                 return false;
334             }
335             if (group.removePreference(preference)) {
336                 // Removal was successful.
337                 return true;
338             }
339 
340             for (int i = 0; i < group.getPreferenceCount(); ++i) {
341                 Preference pref = group.getPreference(i);
342                 if (pref instanceof PreferenceGroup) {
343                     if (recursiveDelete((PreferenceGroup) pref, preference)) {
344                         return true;
345                     }
346                 }
347             }
348             return false;
349         }
350 
351         @Override
onPause()352         public void onPause() {
353             super.onPause();
354             getPreferenceScreen().getSharedPreferences()
355                     .unregisterOnSharedPreferenceChangeListener(this);
356         }
357 
358         @Override
onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key)359         public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
360             setSummary(findPreference(key));
361             if (key.equals(Keys.KEY_RECORD_LOCATION)
362                     && sharedPreferences.getString(key, "0").equals("1")) {
363                 Context context = this.getActivity().getApplicationContext();
364                 if (context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
365                         != PackageManager.PERMISSION_GRANTED) {
366                     requestPermissions(new String[] {Manifest.permission.ACCESS_COARSE_LOCATION},
367                         PERMISSION_REQUEST_CODE);
368                 }
369             }
370         }
371 
372         /**
373          * Set the entries for the given preference. The given preference needs
374          * to be a {@link ListPreference}
375          */
setEntries(Preference preference)376         private void setEntries(Preference preference) {
377             if (!(preference instanceof ListPreference)) {
378                 return;
379             }
380 
381             ListPreference listPreference = (ListPreference) preference;
382             if (listPreference.getKey().equals(Keys.KEY_PICTURE_SIZE_BACK)) {
383                 setEntriesForSelection(mPictureSizes.backCameraSizes, listPreference);
384             } else if (listPreference.getKey().equals(Keys.KEY_PICTURE_SIZE_FRONT)) {
385                 setEntriesForSelection(mPictureSizes.frontCameraSizes, listPreference);
386             } else if (listPreference.getKey().equals(Keys.KEY_VIDEO_QUALITY_BACK)) {
387                 setEntriesForSelection(mPictureSizes.videoQualitiesBack.orNull(), listPreference);
388             } else if (listPreference.getKey().equals(Keys.KEY_VIDEO_QUALITY_FRONT)) {
389                 setEntriesForSelection(mPictureSizes.videoQualitiesFront.orNull(), listPreference);
390             }
391         }
392 
393         /**
394          * Set the summary for the given preference. The given preference needs
395          * to be a {@link ListPreference}.
396          */
setSummary(Preference preference)397         private void setSummary(Preference preference) {
398             if (!(preference instanceof ListPreference)) {
399                 return;
400             }
401 
402             ListPreference listPreference = (ListPreference) preference;
403             if (listPreference.getKey().equals(Keys.KEY_PICTURE_SIZE_BACK)) {
404                 setSummaryForSelection(mPictureSizes.backCameraSizes,
405                         listPreference);
406             } else if (listPreference.getKey().equals(Keys.KEY_PICTURE_SIZE_FRONT)) {
407                 setSummaryForSelection(mPictureSizes.frontCameraSizes,
408                         listPreference);
409             } else if (listPreference.getKey().equals(Keys.KEY_VIDEO_QUALITY_BACK)) {
410                 setSummaryForSelection(mPictureSizes.videoQualitiesBack.orNull(), listPreference);
411             } else if (listPreference.getKey().equals(Keys.KEY_VIDEO_QUALITY_FRONT)) {
412                 setSummaryForSelection(mPictureSizes.videoQualitiesFront.orNull(), listPreference);
413             } else {
414                 listPreference.setSummary(listPreference.getEntry());
415             }
416         }
417 
418         /**
419          * Sets the entries for the given list preference.
420          *
421          * @param selectedSizes The possible S,M,L entries the user can choose
422          *            from.
423          * @param preference The preference to set the entries for.
424          */
setEntriesForSelection(List<Size> selectedSizes, ListPreference preference)425         private void setEntriesForSelection(List<Size> selectedSizes,
426                 ListPreference preference) {
427             if (selectedSizes == null) {
428                 return;
429             }
430 
431             String[] entries = new String[selectedSizes.size()];
432             String[] entryValues = new String[selectedSizes.size()];
433             for (int i = 0; i < selectedSizes.size(); i++) {
434                 Size size = selectedSizes.get(i);
435                 entries[i] = getSizeSummaryString(size);
436                 entryValues[i] = SettingsUtil.sizeToSettingString(size);
437             }
438             preference.setEntries(entries);
439             preference.setEntryValues(entryValues);
440         }
441 
442         /**
443          * Sets the entries for the given list preference.
444          *
445          * @param selectedQualities The possible S,M,L entries the user can
446          *            choose from.
447          * @param preference The preference to set the entries for.
448          */
setEntriesForSelection(SelectedVideoQualities selectedQualities, ListPreference preference)449         private void setEntriesForSelection(SelectedVideoQualities selectedQualities,
450                 ListPreference preference) {
451             if (selectedQualities == null) {
452                 return;
453             }
454 
455             // Avoid adding double entries at the bottom of the list which
456             // indicates that not at least 3 qualities are supported.
457             ArrayList<String> entries = new ArrayList<String>();
458             entries.add(mCamcorderProfileNames[selectedQualities.large]);
459             if (selectedQualities.medium != selectedQualities.large) {
460                 entries.add(mCamcorderProfileNames[selectedQualities.medium]);
461             }
462             if (selectedQualities.small != selectedQualities.medium) {
463                 entries.add(mCamcorderProfileNames[selectedQualities.small]);
464             }
465             preference.setEntries(entries.toArray(new String[0]));
466         }
467 
468         /**
469          * Sets the summary for the given list preference.
470          *
471          * @param displayableSizes The human readable preferred sizes
472          * @param preference The preference for which to set the summary.
473          */
setSummaryForSelection(List<Size> displayableSizes, ListPreference preference)474         private void setSummaryForSelection(List<Size> displayableSizes,
475                                             ListPreference preference) {
476             String setting = preference.getValue();
477             if (setting == null || !setting.contains("x")) {
478                 return;
479             }
480             Size settingSize = SettingsUtil.sizeFromSettingString(setting);
481             if (settingSize == null || settingSize.area() == 0) {
482                 return;
483             }
484             preference.setSummary(getSizeSummaryString(settingSize));
485         }
486 
487         /**
488          * Sets the summary for the given list preference.
489          *
490          * @param selectedQualities The selected video qualities.
491          * @param preference The preference for which to set the summary.
492          */
setSummaryForSelection(SelectedVideoQualities selectedQualities, ListPreference preference)493         private void setSummaryForSelection(SelectedVideoQualities selectedQualities,
494                 ListPreference preference) {
495             if (selectedQualities == null) {
496                 return;
497             }
498 
499             int selectedQuality = selectedQualities.getFromSetting(preference.getValue());
500             preference.setSummary(mCamcorderProfileNames[selectedQuality]);
501         }
502 
503         /**
504          * This method gets the selected picture sizes for S,M,L and populates
505          * {@link #mPictureSizes} accordingly.
506          */
loadSizes()507         private void loadSizes() {
508             if (mInfos == null) {
509                 Log.w(TAG, "null deviceInfo, cannot display resolution sizes");
510                 return;
511             }
512             PictureSizeLoader loader = new PictureSizeLoader(getActivity().getApplicationContext());
513             mPictureSizes = loader.computePictureSizes();
514             loader.release();
515         }
516 
517         /**
518          * @param size The photo resolution.
519          * @return A human readable and translated string for labeling the
520          *         picture size in megapixels.
521          */
getSizeSummaryString(Size size)522         private String getSizeSummaryString(Size size) {
523             Size approximateSize = ResolutionUtil.getApproximateSize(size);
524             String megaPixels = sMegaPixelFormat.format((size.width() * size.height()) / 1e6);
525             int numerator = ResolutionUtil.aspectRatioNumerator(approximateSize);
526             int denominator = ResolutionUtil.aspectRatioDenominator(approximateSize);
527             String result = getResources().getString(
528                     R.string.setting_summary_aspect_ratio_and_megapixels, numerator, denominator,
529                     megaPixels);
530             return result;
531         }
532     }
533 }
534