SettingsNotificationsFragment.java 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. /* _____ _
  2. * |_ _| |_ _ _ ___ ___ _ __ __ _
  3. * | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. * |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. *
  6. * Threema for Android
  7. * Copyright (c) 2013-2021 Threema GmbH
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License, version 3,
  11. * as published by the Free Software Foundation.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU Affero General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public License
  19. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. */
  21. package ch.threema.app.preference;
  22. import android.app.Activity;
  23. import android.content.ActivityNotFoundException;
  24. import android.content.ComponentName;
  25. import android.content.Intent;
  26. import android.content.SharedPreferences;
  27. import android.media.RingtoneManager;
  28. import android.net.Uri;
  29. import android.os.Build;
  30. import android.os.Bundle;
  31. import android.provider.Settings;
  32. import android.text.format.DateFormat;
  33. import android.view.View;
  34. import com.google.android.material.timepicker.MaterialTimePicker;
  35. import org.slf4j.Logger;
  36. import org.slf4j.LoggerFactory;
  37. import java.text.DateFormatSymbols;
  38. import java.util.Arrays;
  39. import java.util.Locale;
  40. import java.util.Set;
  41. import androidx.activity.result.ActivityResultLauncher;
  42. import androidx.activity.result.contract.ActivityResultContracts;
  43. import androidx.annotation.Nullable;
  44. import androidx.annotation.StringRes;
  45. import androidx.core.app.NotificationManagerCompat;
  46. import androidx.preference.CheckBoxPreference;
  47. import androidx.preference.MultiSelectListPreference;
  48. import androidx.preference.Preference;
  49. import androidx.preference.PreferenceCategory;
  50. import androidx.preference.PreferenceScreen;
  51. import ch.threema.app.BuildConfig;
  52. import ch.threema.app.R;
  53. import ch.threema.app.ThreemaApplication;
  54. import ch.threema.app.dialogs.GenericAlertDialog;
  55. import ch.threema.app.dialogs.RingtoneSelectorDialog;
  56. import ch.threema.app.dialogs.ShowOnceDialog;
  57. import ch.threema.app.utils.AppRestrictionUtil;
  58. import ch.threema.app.utils.ConfigUtils;
  59. import ch.threema.app.utils.RingtoneUtil;
  60. import static com.google.android.material.timepicker.TimeFormat.CLOCK_12H;
  61. import static com.google.android.material.timepicker.TimeFormat.CLOCK_24H;
  62. public class SettingsNotificationsFragment extends ThreemaPreferenceFragment implements GenericAlertDialog.DialogClickListener, RingtoneSelectorDialog.RingtoneSelectorDialogClickListener {
  63. private static final Logger logger = LoggerFactory.getLogger(SettingsNotificationsFragment.class);
  64. private static final String DIALOG_TAG_NOTIFICATIONS_DISABLED = "ndd";
  65. private static final String DIALOG_TAG_CONTACT_NOTIFICATION = "cn";
  66. private static final String DIALOG_TAG_GROUP_NOTIFICATION = "gn";
  67. private static final String DIALOG_TAG_VOIP_NOTIFICATION = "vn";
  68. private static final String DIALOG_TAG_MIUI_NOTICE = "miui10_channel_notice";
  69. private static final int INTENT_SYSTEM_NOTIFICATION_SETTINGS = 5199;
  70. private SharedPreferences sharedPreferences;
  71. private NotificationManagerCompat notificationManagerCompat;
  72. // Weekdays used for work-life balance prefs
  73. private final String[] weekdays = new String[7];
  74. private final String[] shortWeekdays = new String[7];
  75. private final String[] weekday_values = new String[]{"0", "1", "2", "3", "4", "5", "6"};
  76. private Preference startPreference, endPreference;
  77. private Preference ringtonePreference, groupRingtonePreference, voiceRingtonePreference;
  78. private final ActivityResultLauncher<Intent> voipRingtonePickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
  79. result -> {
  80. if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
  81. Uri uri = result.getData().getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
  82. onRingtoneSelected(DIALOG_TAG_VOIP_NOTIFICATION, uri);
  83. }
  84. });
  85. private final ActivityResultLauncher<Intent> contactTonePickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
  86. result -> {
  87. if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
  88. Uri uri = result.getData().getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
  89. onRingtoneSelected(DIALOG_TAG_CONTACT_NOTIFICATION, uri);
  90. }
  91. });
  92. private final ActivityResultLauncher<Intent> groupTonePickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
  93. result -> {
  94. if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
  95. Uri uri = result.getData().getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
  96. onRingtoneSelected(DIALOG_TAG_GROUP_NOTIFICATION, uri);
  97. }
  98. });
  99. private void initWorkingTimePrefs() {
  100. if (!ConfigUtils.isWorkBuild()) {
  101. // remove preferences
  102. PreferenceScreen preferenceScreen = findPreference("pref_key_notifications");
  103. PreferenceCategory preferenceCategory = findPreference("pref_key_work_life_balance");
  104. preferenceScreen.removePreference(preferenceCategory);
  105. return;
  106. }
  107. DateFormatSymbols dfs = new DateFormatSymbols(getResources().getConfiguration().locale);
  108. System.arraycopy(dfs.getWeekdays(), 1, weekdays, 0, 7);
  109. System.arraycopy(dfs.getShortWeekdays(), 1, shortWeekdays, 0, 7);
  110. MultiSelectListPreference multiSelectListPreference = findPreference(getString(R.string.preferences__working_days));
  111. multiSelectListPreference.setEntries(weekdays);
  112. multiSelectListPreference.setOnPreferenceChangeListener((preference, newValue) -> {
  113. updateWorkingDaysSummary(preference, (Set<String>)newValue);
  114. return true;
  115. });
  116. updateWorkingDaysSummary(multiSelectListPreference, multiSelectListPreference.getValues());
  117. startPreference = findPreference(getString(R.string.preferences__work_time_start));
  118. updateTimeSummary(startPreference, R.string.prefs_work_time_start_sum);
  119. startPreference.setOnPreferenceClickListener(preference -> {
  120. int[] startTime = splitDateFromPrefs(R.string.preferences__work_time_start);
  121. final MaterialTimePicker timePicker = new MaterialTimePicker.Builder()
  122. .setTitleText(R.string.prefs_work_time_start)
  123. .setHour(startTime != null ? startTime[0] : 0)
  124. .setMinute(startTime != null ? startTime[1] : 0)
  125. .setTimeFormat(DateFormat.is24HourFormat(getContext()) ? CLOCK_24H : CLOCK_12H)
  126. .build();
  127. timePicker.addOnPositiveButtonClickListener(v1 -> {
  128. int[] endTime = splitDateFromPrefs(R.string.preferences__work_time_end);
  129. if (endTime != null) {
  130. int newTimeStamp = timePicker.getHour() * 60 + timePicker.getMinute();
  131. int endTimeStamp = endTime[0] * 60 + endTime[1];
  132. if (newTimeStamp >= endTimeStamp) {
  133. return;
  134. }
  135. }
  136. String newValue = String.format(Locale.US, "%02d:%02d", timePicker.getHour(), timePicker.getMinute());
  137. sharedPreferences.edit().putString(getResources().getString(R.string.preferences__work_time_start), newValue).apply();
  138. updateTimeSummary(startPreference, R.string.prefs_work_time_start_sum);
  139. });
  140. if (isAdded()) {
  141. timePicker.show(getParentFragmentManager(), "startt");
  142. }
  143. return true;
  144. });
  145. endPreference = findPreference(getString(R.string.preferences__work_time_end));
  146. updateTimeSummary(endPreference, R.string.prefs_work_time_end_sum);
  147. endPreference.setOnPreferenceClickListener(preference -> {
  148. int[] endTime = splitDateFromPrefs(R.string.preferences__work_time_end);
  149. final MaterialTimePicker timePicker = new MaterialTimePicker.Builder()
  150. .setTitleText(R.string.prefs_work_time_end)
  151. .setHour(endTime != null ? endTime[0] : 0)
  152. .setMinute(endTime != null ? endTime[1] : 0)
  153. .setTimeFormat(DateFormat.is24HourFormat(getContext()) ? CLOCK_24H : CLOCK_12H)
  154. .build();
  155. timePicker.addOnPositiveButtonClickListener(v1 -> {
  156. int[] startTime = splitDateFromPrefs(R.string.preferences__work_time_start);
  157. if (startTime != null) {
  158. int newTimeStamp = timePicker.getHour() * 60 + timePicker.getMinute();
  159. int startTimeStamp = startTime[0] * 60 + startTime[1];
  160. if (newTimeStamp <= startTimeStamp) {
  161. return;
  162. }
  163. }
  164. String newValue = String.format(Locale.US, "%02d:%02d", timePicker.getHour(), timePicker.getMinute());
  165. sharedPreferences.edit().putString(getResources().getString(R.string.preferences__work_time_end), newValue).apply();
  166. updateTimeSummary(endPreference, R.string.prefs_work_time_end_sum);
  167. });
  168. if (isAdded()) {
  169. timePicker.show(getParentFragmentManager(), "endt");
  170. }
  171. return true;
  172. });
  173. }
  174. private void updateWorkingDaysSummary(Preference preference, Set<String> values) {
  175. StringBuilder summary = new StringBuilder();
  176. for (String value : values) {
  177. int index = Arrays.asList(weekday_values).indexOf(value);
  178. if (summary.length() > 0) {
  179. summary.append(", ");
  180. }
  181. summary.append(shortWeekdays[index]);
  182. }
  183. if (summary.length() == 0) {
  184. summary = new StringBuilder(getString(R.string.prefs_working_days_sum));
  185. }
  186. preference.setSummary(summary);
  187. }
  188. @Nullable
  189. private int[] splitDateFromPrefs(@StringRes int key) {
  190. String value = sharedPreferences.getString(getString(key), null);
  191. if (value == null) {
  192. return null;
  193. }
  194. try {
  195. String[] hourMinuteString = value.split(":");
  196. int[] hourMinuteInt = new int[2];
  197. hourMinuteInt[0] = Integer.parseInt(hourMinuteString[0]);
  198. hourMinuteInt[1] = Integer.parseInt(hourMinuteString[1]);
  199. return hourMinuteInt;
  200. } catch (Exception e) {
  201. return null;
  202. }
  203. }
  204. @Override
  205. public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String rootKey) {
  206. sharedPreferences = getPreferenceManager().getSharedPreferences();
  207. addPreferencesFromResource(R.xml.preference_notifications);
  208. int miuiVersion = ConfigUtils.getMIUIVersion();
  209. if (miuiVersion < 10) {
  210. PreferenceScreen preferenceScreen = findPreference("pref_key_notifications");
  211. preferenceScreen.removePreference(findPreference("pref_key_miui"));
  212. }
  213. notificationManagerCompat = NotificationManagerCompat.from(getActivity());
  214. initWorkingTimePrefs();
  215. // setup defaults and callbacks
  216. ringtonePreference = findPreference(getResources().getString(R.string.preferences__notification_sound));
  217. updateRingtoneSummary(ringtonePreference, sharedPreferences.getString(getResources().getString(R.string.preferences__notification_sound), ""));
  218. groupRingtonePreference = findPreference(getResources().getString(R.string.preferences__group_notification_sound));
  219. updateRingtoneSummary(groupRingtonePreference, sharedPreferences.getString(getResources().getString(R.string.preferences__group_notification_sound), ""));
  220. voiceRingtonePreference = findPreference(getResources().getString(R.string.preferences__voip_ringtone));
  221. updateRingtoneSummary(voiceRingtonePreference, sharedPreferences.getString(getResources().getString(R.string.preferences__voip_ringtone), ""));
  222. ringtonePreference.setOnPreferenceClickListener(preference -> {
  223. chooseRingtone(RingtoneManager.TYPE_NOTIFICATION,
  224. getRingtoneFromRingtonePref(R.string.preferences__notification_sound),
  225. null,
  226. getString(R.string.prefs_notification_sound),
  227. DIALOG_TAG_CONTACT_NOTIFICATION);
  228. return true;
  229. });
  230. groupRingtonePreference.setOnPreferenceClickListener(preference -> {
  231. chooseRingtone(RingtoneManager.TYPE_NOTIFICATION,
  232. getRingtoneFromRingtonePref(R.string.preferences__group_notification_sound),
  233. null,
  234. getString(R.string.prefs_notification_sound),
  235. DIALOG_TAG_GROUP_NOTIFICATION);
  236. return true;
  237. });
  238. voiceRingtonePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
  239. @Override
  240. public boolean onPreferenceClick(Preference preference) {
  241. chooseRingtone(RingtoneManager.TYPE_RINGTONE, getRingtoneFromRingtonePref(R.string.preferences__voip_ringtone), RingtoneUtil.THREEMA_CALL_RINGTONE_URI, getString(R.string.prefs_voice_call_sound), DIALOG_TAG_VOIP_NOTIFICATION);
  242. return true;
  243. }
  244. });
  245. if (ConfigUtils.isWorkRestricted()) {
  246. CheckBoxPreference notificationPreview = findPreference(getString(R.string.preferences__notification_preview));
  247. Boolean value = AppRestrictionUtil.getBooleanRestriction(getString(R.string.restriction__disable_message_preview));
  248. if (value != null) {
  249. notificationPreview.setEnabled(false);
  250. notificationPreview.setSelectable(false);
  251. }
  252. }
  253. if (miuiVersion >= 10) {
  254. ShowOnceDialog.newInstance(
  255. R.string.miui_notification_title,
  256. miuiVersion >= 12 ?
  257. R.string.miui12_notification_body:
  258. R.string.miui_notification_body).show(getFragmentManager(), DIALOG_TAG_MIUI_NOTICE);
  259. Preference miuiPreference = findPreference("pref_key_miui");
  260. miuiPreference.setOnPreferenceClickListener(preference -> {
  261. openMIUINotificationSettings();
  262. return true;
  263. });
  264. }
  265. }
  266. private void chooseRingtone(final int type, final Uri currentUri, final Uri defaultUri, final String title, final String tag) {
  267. try {
  268. Intent intent = RingtoneUtil.getRingtonePickerIntent(type, currentUri, defaultUri);
  269. switch (tag) {
  270. case DIALOG_TAG_VOIP_NOTIFICATION:
  271. voipRingtonePickerLauncher.launch(intent);
  272. break;
  273. case DIALOG_TAG_CONTACT_NOTIFICATION:
  274. contactTonePickerLauncher.launch(intent);
  275. break;
  276. case DIALOG_TAG_GROUP_NOTIFICATION:
  277. groupTonePickerLauncher.launch(intent);
  278. break;
  279. }
  280. } catch (ActivityNotFoundException e) {
  281. RingtoneSelectorDialog dialog = RingtoneSelectorDialog.newInstance(
  282. title,
  283. type,
  284. currentUri,
  285. defaultUri,
  286. true,
  287. true);
  288. dialog.setTargetFragment(SettingsNotificationsFragment.this, 0);
  289. dialog.show(getFragmentManager(), tag);
  290. }
  291. }
  292. @Override
  293. public void onViewCreated(View view, Bundle savedInstanceState) {
  294. preferenceFragmentCallbackInterface.setToolbarTitle(R.string.prefs_notifications);
  295. super.onViewCreated(view, savedInstanceState);
  296. if (!notificationManagerCompat.areNotificationsEnabled()) {
  297. showNotificationsDisabledDialog();
  298. }
  299. }
  300. private void showNotificationsDisabledDialog() {
  301. GenericAlertDialog dialog = GenericAlertDialog.newInstance(R.string.notifications_disabled_title, R.string.notifications_disabled_text, R.string.notifications_disabled_settings, R.string.cancel);
  302. dialog.setTargetFragment(this, 0);
  303. dialog.show(getFragmentManager(), DIALOG_TAG_NOTIFICATIONS_DISABLED);
  304. }
  305. private Uri getRingtoneFromRingtonePref(@StringRes int preference) {
  306. String uriString = sharedPreferences.getString(getResources().getString(preference), null);
  307. if (uriString == null) {
  308. // silent
  309. uriString = "";
  310. }
  311. return Uri.parse(uriString);
  312. }
  313. private void updateRingtoneSummary(Preference preference, String value) {
  314. String summary = null;
  315. if (value == null || value.length() == 0) {
  316. summary = getString(R.string.ringtone_none);
  317. } else {
  318. summary = RingtoneUtil.getRingtoneNameFromUri(getContext(), Uri.parse(value));
  319. }
  320. preference.setSummary(summary);
  321. }
  322. private void updateTimeSummary(Preference preference, @StringRes int defaultSummary) {
  323. preference.setSummary(sharedPreferences.getString(preference.getKey(), getString(defaultSummary)));
  324. }
  325. private void openMIUINotificationSettings() {
  326. ComponentName cn = new ComponentName("com.android.settings", "com.android.settings.Settings$NotificationFilterActivity");
  327. Bundle bundle = new Bundle();
  328. bundle.putString("appName", getContext().getResources().getString(getContext().getApplicationInfo().labelRes));
  329. bundle.putString("packageName", BuildConfig.APPLICATION_ID);
  330. bundle.putString(":android:show_fragment", "NotificationAccessSettings");
  331. Intent intent = new Intent();
  332. intent.putExtras(bundle);
  333. intent.setComponent(cn);
  334. try {
  335. startActivity(intent);
  336. } catch (Exception e) {
  337. logger.error("Exception", e);
  338. }
  339. }
  340. @Override
  341. public void onYes(String tag, Object data) {
  342. Intent intent = new Intent();
  343. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
  344. intent.setAction("android.settings.APP_NOTIFICATION_SETTINGS");
  345. // for Android 5-7
  346. intent.putExtra("app_package", getActivity().getPackageName());
  347. intent.putExtra("app_uid", getActivity().getApplicationInfo().uid);
  348. // for Android O
  349. intent.putExtra("android.provider.extra.APP_PACKAGE", getActivity().getPackageName());
  350. } else {
  351. intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
  352. intent.addCategory(Intent.CATEGORY_DEFAULT);
  353. intent.setData(Uri.parse("package:" + getActivity().getPackageName()));
  354. }
  355. startActivityForResult(intent, INTENT_SYSTEM_NOTIFICATION_SETTINGS);
  356. }
  357. @Override
  358. public void onNo(String tag, Object data) {
  359. // ignore disabled notifications
  360. }
  361. @Override
  362. public void onActivityResult(int requestCode, int resultCode, Intent data) {
  363. super.onActivityResult(requestCode, resultCode, data);
  364. if (requestCode == INTENT_SYSTEM_NOTIFICATION_SETTINGS && !notificationManagerCompat.areNotificationsEnabled()) {
  365. // return from system settings but notifications still disabled
  366. showNotificationsDisabledDialog();
  367. }
  368. }
  369. @Override
  370. public void onRingtoneSelected(String tag, Uri ringtone) {
  371. String toneString = ringtone != null ? ringtone.toString() : "";
  372. switch (tag) {
  373. case DIALOG_TAG_CONTACT_NOTIFICATION:
  374. sharedPreferences.edit().putString(ThreemaApplication.getAppContext().getString(R.string.preferences__notification_sound), toneString).apply();
  375. updateRingtoneSummary(ringtonePreference, sharedPreferences.getString(getResources().getString(R.string.preferences__notification_sound), ""));
  376. break;
  377. case DIALOG_TAG_GROUP_NOTIFICATION:
  378. sharedPreferences.edit().putString(ThreemaApplication.getAppContext().getString(R.string.preferences__group_notification_sound), toneString).apply();
  379. updateRingtoneSummary(groupRingtonePreference, sharedPreferences.getString(getResources().getString(R.string.preferences__group_notification_sound), ""));
  380. break;
  381. case DIALOG_TAG_VOIP_NOTIFICATION:
  382. sharedPreferences.edit().putString(ThreemaApplication.getAppContext().getString(R.string.preferences__voip_ringtone), toneString).apply();
  383. updateRingtoneSummary(voiceRingtonePreference, sharedPreferences.getString(getResources().getString(R.string.preferences__voip_ringtone), ""));
  384. break;
  385. }
  386. }
  387. @Override
  388. public void onCancel(String tag) {}
  389. }