The issues is that TimeZoneData is being loaded with LoaderManager on every fragment's onCreate(). At the same time this fragment has onActivityResult() handler that accesses TimeZoneData and relies on the fact that it has been already initialized prior this call. So when you enable "Don't keep activities" mode, the TimeZoneSettings fragment [1] that holds TimeZoneData is immediately destroyed after launching time zone picker fragment [2] (which result [1] waits for), and when android calls onActivityResult() for [1] (after time zone has been picked in [2]), Android has to recreate [1] from scratch. So you get onCreate() called and immidiately after that onActivityResult() called, and at this point we get the crash, because the loader hasn't finished yet and onActivityResult() tries to access uninitialized mTimeZoneData. This CL fixes this crash issue by postponing mTimeZoneData.lookupCountryTimeZones call until mTimeZoneData is loaded by loader. It is done through saving data intent in onActivityResult() and calling onZonePickerRequestResult() when time zone data is loaded. Bug: 112351583 Test: m Settings droid successfully builds, manual testing with "Don't keep activities" mode enabled Change-Id: Id30d36dc17a0de96ba871f9d0f4a2686479d0eeb
387 lines
15 KiB
Java
387 lines
15 KiB
Java
/*
|
|
* Copyright (C) 2018 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.android.settings.datetime.timezone;
|
|
|
|
import android.app.Activity;
|
|
import android.app.AlarmManager;
|
|
import android.app.settings.SettingsEnums;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.SharedPreferences;
|
|
import android.icu.util.TimeZone;
|
|
import android.os.Bundle;
|
|
import android.util.Log;
|
|
import android.view.Menu;
|
|
import android.view.MenuInflater;
|
|
import android.view.MenuItem;
|
|
|
|
import androidx.annotation.VisibleForTesting;
|
|
import androidx.preference.PreferenceCategory;
|
|
|
|
import com.android.settings.R;
|
|
import com.android.settings.core.SubSettingLauncher;
|
|
import com.android.settings.dashboard.DashboardFragment;
|
|
import com.android.settings.datetime.timezone.model.FilteredCountryTimeZones;
|
|
import com.android.settings.datetime.timezone.model.TimeZoneData;
|
|
import com.android.settings.datetime.timezone.model.TimeZoneDataLoader;
|
|
import com.android.settingslib.core.AbstractPreferenceController;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Date;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Objects;
|
|
import java.util.Set;
|
|
|
|
/**
|
|
* The class displays a time zone picker either by regions or fixed offset time zones.
|
|
*/
|
|
public class TimeZoneSettings extends DashboardFragment {
|
|
|
|
private static final String TAG = "TimeZoneSettings";
|
|
|
|
private static final int MENU_BY_REGION = Menu.FIRST;
|
|
private static final int MENU_BY_OFFSET = Menu.FIRST + 1;
|
|
|
|
private static final int REQUEST_CODE_REGION_PICKER = 1;
|
|
private static final int REQUEST_CODE_ZONE_PICKER = 2;
|
|
private static final int REQUEST_CODE_FIXED_OFFSET_ZONE_PICKER = 3;
|
|
|
|
private static final String PREF_KEY_REGION = "time_zone_region";
|
|
private static final String PREF_KEY_REGION_CATEGORY = "time_zone_region_preference_category";
|
|
private static final String PREF_KEY_FIXED_OFFSET_CATEGORY =
|
|
"time_zone_fixed_offset_preference_category";
|
|
|
|
private Locale mLocale;
|
|
private boolean mSelectByRegion;
|
|
private TimeZoneData mTimeZoneData;
|
|
private Intent mPendingZonePickerRequestResult;
|
|
|
|
private String mSelectedTimeZoneId;
|
|
private TimeZoneInfo.Formatter mTimeZoneInfoFormatter;
|
|
|
|
@Override
|
|
public int getMetricsCategory() {
|
|
return SettingsEnums.ZONE_PICKER;
|
|
}
|
|
|
|
@Override
|
|
protected int getPreferenceScreenResId() {
|
|
return R.xml.time_zone_prefs;
|
|
}
|
|
|
|
@Override
|
|
protected String getLogTag() {
|
|
return TAG;
|
|
}
|
|
|
|
/**
|
|
* Called during onAttach
|
|
*/
|
|
@VisibleForTesting
|
|
@Override
|
|
public List<AbstractPreferenceController> createPreferenceControllers(Context context) {
|
|
mLocale = context.getResources().getConfiguration().getLocales().get(0);
|
|
mTimeZoneInfoFormatter = new TimeZoneInfo.Formatter(mLocale, new Date());
|
|
final List<AbstractPreferenceController> controllers = new ArrayList<>();
|
|
RegionPreferenceController regionPreferenceController =
|
|
new RegionPreferenceController(context);
|
|
regionPreferenceController.setOnClickListener(this::startRegionPicker);
|
|
RegionZonePreferenceController regionZonePreferenceController =
|
|
new RegionZonePreferenceController(context);
|
|
regionZonePreferenceController.setOnClickListener(this::onRegionZonePreferenceClicked);
|
|
FixedOffsetPreferenceController fixedOffsetPreferenceController =
|
|
new FixedOffsetPreferenceController(context);
|
|
fixedOffsetPreferenceController.setOnClickListener(this::startFixedOffsetPicker);
|
|
|
|
controllers.add(regionPreferenceController);
|
|
controllers.add(regionZonePreferenceController);
|
|
controllers.add(fixedOffsetPreferenceController);
|
|
return controllers;
|
|
}
|
|
|
|
@Override
|
|
public void onCreate(Bundle icicle) {
|
|
super.onCreate(icicle);
|
|
// Hide all interactive preferences
|
|
setPreferenceCategoryVisible((PreferenceCategory) findPreference(
|
|
PREF_KEY_REGION_CATEGORY), false);
|
|
setPreferenceCategoryVisible((PreferenceCategory) findPreference(
|
|
PREF_KEY_FIXED_OFFSET_CATEGORY), false);
|
|
|
|
// Start loading TimeZoneData
|
|
getLoaderManager().initLoader(0, null, new TimeZoneDataLoader.LoaderCreator(
|
|
getContext(), this::onTimeZoneDataReady));
|
|
}
|
|
|
|
@Override
|
|
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
if (resultCode != Activity.RESULT_OK || data == null) {
|
|
return;
|
|
}
|
|
|
|
switch (requestCode) {
|
|
case REQUEST_CODE_REGION_PICKER:
|
|
case REQUEST_CODE_ZONE_PICKER: {
|
|
if (mTimeZoneData == null) {
|
|
mPendingZonePickerRequestResult = data;
|
|
} else {
|
|
onZonePickerRequestResult(mTimeZoneData, data);
|
|
}
|
|
break;
|
|
}
|
|
case REQUEST_CODE_FIXED_OFFSET_ZONE_PICKER: {
|
|
String tzId = data.getStringExtra(FixedOffsetPicker.EXTRA_RESULT_TIME_ZONE_ID);
|
|
// Ignore the result if user didn't change the time zone.
|
|
if (tzId != null && !tzId.equals(mSelectedTimeZoneId)) {
|
|
onFixedOffsetZoneChanged(tzId);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
@VisibleForTesting
|
|
void setTimeZoneData(TimeZoneData timeZoneData) {
|
|
mTimeZoneData = timeZoneData;
|
|
}
|
|
|
|
private void onTimeZoneDataReady(TimeZoneData timeZoneData) {
|
|
if (mTimeZoneData == null && timeZoneData != null) {
|
|
mTimeZoneData = timeZoneData;
|
|
setupForCurrentTimeZone();
|
|
getActivity().invalidateOptionsMenu();
|
|
if (mPendingZonePickerRequestResult != null) {
|
|
onZonePickerRequestResult(timeZoneData, mPendingZonePickerRequestResult);
|
|
mPendingZonePickerRequestResult = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void startRegionPicker() {
|
|
startPickerFragment(RegionSearchPicker.class, new Bundle(), REQUEST_CODE_REGION_PICKER);
|
|
}
|
|
|
|
private void onRegionZonePreferenceClicked() {
|
|
final Bundle args = new Bundle();
|
|
args.putString(RegionZonePicker.EXTRA_REGION_ID,
|
|
use(RegionPreferenceController.class).getRegionId());
|
|
startPickerFragment(RegionZonePicker.class, args, REQUEST_CODE_ZONE_PICKER);
|
|
}
|
|
|
|
private void startFixedOffsetPicker() {
|
|
startPickerFragment(FixedOffsetPicker.class, new Bundle(),
|
|
REQUEST_CODE_FIXED_OFFSET_ZONE_PICKER);
|
|
}
|
|
|
|
private void startPickerFragment(Class<? extends BaseTimeZonePicker> fragmentClass, Bundle args,
|
|
int resultRequestCode) {
|
|
new SubSettingLauncher(getContext())
|
|
.setDestination(fragmentClass.getCanonicalName())
|
|
.setArguments(args)
|
|
.setSourceMetricsCategory(getMetricsCategory())
|
|
.setResultListener(this, resultRequestCode)
|
|
.launch();
|
|
}
|
|
|
|
private void setDisplayedRegion(String regionId) {
|
|
use(RegionPreferenceController.class).setRegionId(regionId);
|
|
updatePreferenceStates();
|
|
}
|
|
|
|
private void setDisplayedTimeZoneInfo(String regionId, String tzId) {
|
|
final TimeZoneInfo tzInfo = tzId == null ? null : mTimeZoneInfoFormatter.format(tzId);
|
|
final FilteredCountryTimeZones countryTimeZones =
|
|
mTimeZoneData.lookupCountryTimeZones(regionId);
|
|
|
|
use(RegionZonePreferenceController.class).setTimeZoneInfo(tzInfo);
|
|
// Only clickable when the region has more than 1 time zones or no time zone is selected.
|
|
|
|
use(RegionZonePreferenceController.class).setClickable(tzInfo == null ||
|
|
(countryTimeZones != null && countryTimeZones.getTimeZoneIds().size() > 1));
|
|
use(TimeZoneInfoPreferenceController.class).setTimeZoneInfo(tzInfo);
|
|
|
|
updatePreferenceStates();
|
|
}
|
|
|
|
private void setDisplayedFixedOffsetTimeZoneInfo(String tzId) {
|
|
if (isFixedOffset(tzId)) {
|
|
use(FixedOffsetPreferenceController.class).setTimeZoneInfo(
|
|
mTimeZoneInfoFormatter.format(tzId));
|
|
} else {
|
|
use(FixedOffsetPreferenceController.class).setTimeZoneInfo(null);
|
|
}
|
|
updatePreferenceStates();
|
|
}
|
|
|
|
private void onZonePickerRequestResult(TimeZoneData timeZoneData, Intent data) {
|
|
String regionId = data.getStringExtra(RegionSearchPicker.EXTRA_RESULT_REGION_ID);
|
|
String tzId = data.getStringExtra(RegionZonePicker.EXTRA_RESULT_TIME_ZONE_ID);
|
|
// Ignore the result if user didn't change the region or time zone.
|
|
if (Objects.equals(regionId, use(RegionPreferenceController.class).getRegionId())
|
|
&& Objects.equals(tzId, mSelectedTimeZoneId)) {
|
|
return;
|
|
}
|
|
|
|
FilteredCountryTimeZones countryTimeZones =
|
|
timeZoneData.lookupCountryTimeZones(regionId);
|
|
if (countryTimeZones == null || !countryTimeZones.getTimeZoneIds().contains(tzId)) {
|
|
Log.e(TAG, "Unknown time zone id is selected: " + tzId);
|
|
return;
|
|
}
|
|
|
|
mSelectedTimeZoneId = tzId;
|
|
setDisplayedRegion(regionId);
|
|
setDisplayedTimeZoneInfo(regionId, mSelectedTimeZoneId);
|
|
saveTimeZone(regionId, mSelectedTimeZoneId);
|
|
|
|
// Switch to the region mode if the user switching from the fixed offset
|
|
setSelectByRegion(true);
|
|
}
|
|
|
|
private void onFixedOffsetZoneChanged(String tzId) {
|
|
mSelectedTimeZoneId = tzId;
|
|
setDisplayedFixedOffsetTimeZoneInfo(tzId);
|
|
saveTimeZone(null, mSelectedTimeZoneId);
|
|
|
|
// Switch to the fixed offset mode if the user switching from the region mode
|
|
setSelectByRegion(false);
|
|
}
|
|
|
|
private void saveTimeZone(String regionId, String tzId) {
|
|
SharedPreferences.Editor editor = getPreferenceManager().getSharedPreferences().edit();
|
|
if (regionId == null) {
|
|
editor.remove(PREF_KEY_REGION);
|
|
} else {
|
|
editor.putString(PREF_KEY_REGION, regionId);
|
|
}
|
|
editor.apply();
|
|
getActivity().getSystemService(AlarmManager.class).setTimeZone(tzId);
|
|
}
|
|
|
|
@Override
|
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
|
menu.add(0, MENU_BY_REGION, 0, R.string.zone_menu_by_region);
|
|
menu.add(0, MENU_BY_OFFSET, 0, R.string.zone_menu_by_offset);
|
|
super.onCreateOptionsMenu(menu, inflater);
|
|
}
|
|
|
|
@Override
|
|
public void onPrepareOptionsMenu(Menu menu) {
|
|
// Do not show menu when data is not ready,
|
|
menu.findItem(MENU_BY_REGION).setVisible(mTimeZoneData != null && !mSelectByRegion);
|
|
menu.findItem(MENU_BY_OFFSET).setVisible(mTimeZoneData != null && mSelectByRegion);
|
|
}
|
|
|
|
@Override
|
|
public boolean onOptionsItemSelected(MenuItem item) {
|
|
switch (item.getItemId()) {
|
|
case MENU_BY_REGION:
|
|
startRegionPicker();
|
|
return true;
|
|
|
|
case MENU_BY_OFFSET:
|
|
startFixedOffsetPicker();
|
|
return true;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void setupForCurrentTimeZone() {
|
|
mSelectedTimeZoneId = TimeZone.getDefault().getID();
|
|
setSelectByRegion(!isFixedOffset(mSelectedTimeZoneId));
|
|
}
|
|
|
|
private static boolean isFixedOffset(String tzId) {
|
|
return tzId.startsWith("Etc/GMT") || tzId.equals("Etc/UTC");
|
|
}
|
|
|
|
/**
|
|
* Switch the current view to select region or select fixed offset time zone.
|
|
* When showing the selected region, it guess the selected region from time zone id.
|
|
* See {@link #findRegionIdForTzId} for more info.
|
|
*/
|
|
private void setSelectByRegion(boolean selectByRegion) {
|
|
mSelectByRegion = selectByRegion;
|
|
setPreferenceCategoryVisible((PreferenceCategory) findPreference(
|
|
PREF_KEY_REGION_CATEGORY), selectByRegion);
|
|
setPreferenceCategoryVisible((PreferenceCategory) findPreference(
|
|
PREF_KEY_FIXED_OFFSET_CATEGORY), !selectByRegion);
|
|
final String localeRegionId = getLocaleRegionId();
|
|
final Set<String> allCountryIsoCodes = mTimeZoneData.getRegionIds();
|
|
|
|
String displayRegion = allCountryIsoCodes.contains(localeRegionId) ? localeRegionId : null;
|
|
setDisplayedRegion(displayRegion);
|
|
setDisplayedTimeZoneInfo(displayRegion, null);
|
|
|
|
if (!mSelectByRegion) {
|
|
setDisplayedFixedOffsetTimeZoneInfo(mSelectedTimeZoneId);
|
|
return;
|
|
}
|
|
|
|
String regionId = findRegionIdForTzId(mSelectedTimeZoneId);
|
|
if (regionId != null) {
|
|
setDisplayedRegion(regionId);
|
|
setDisplayedTimeZoneInfo(regionId, mSelectedTimeZoneId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find the a region associated with the specified time zone, based on the time zone data.
|
|
* If there are multiple regions associated with the given time zone, the priority will be given
|
|
* to the region the user last picked and the country in user's locale.
|
|
* @return null if no region associated with the time zone
|
|
*/
|
|
private String findRegionIdForTzId(String tzId) {
|
|
return findRegionIdForTzId(tzId,
|
|
getPreferenceManager().getSharedPreferences().getString(PREF_KEY_REGION, null),
|
|
getLocaleRegionId());
|
|
}
|
|
|
|
@VisibleForTesting
|
|
String findRegionIdForTzId(String tzId, String sharePrefRegionId, String localeRegionId) {
|
|
final Set<String> matchedRegions = mTimeZoneData.lookupCountryCodesForZoneId(tzId);
|
|
if (matchedRegions.size() == 0) {
|
|
return null;
|
|
}
|
|
if (sharePrefRegionId != null && matchedRegions.contains(sharePrefRegionId)) {
|
|
return sharePrefRegionId;
|
|
}
|
|
if (localeRegionId != null && matchedRegions.contains(localeRegionId)) {
|
|
return localeRegionId;
|
|
}
|
|
|
|
return matchedRegions.toArray(new String[matchedRegions.size()])[0];
|
|
}
|
|
|
|
private void setPreferenceCategoryVisible(PreferenceCategory category,
|
|
boolean isVisible) {
|
|
// Hiding category doesn't hide all the children preference. Set visibility of its children.
|
|
// Do not care grandchildren as time_zone_pref.xml has only 2 levels.
|
|
category.setVisible(isVisible);
|
|
for (int i = 0; i < category.getPreferenceCount(); i++) {
|
|
category.getPreference(i).setVisible(isVisible);
|
|
}
|
|
}
|
|
|
|
private String getLocaleRegionId() {
|
|
return mLocale.getCountry().toUpperCase(Locale.US);
|
|
}
|
|
}
|