ConfigUtils.java 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569
  1. /* _____ _
  2. * |_ _| |_ _ _ ___ ___ _ __ __ _
  3. * | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. * |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. *
  6. * Threema for Android
  7. * Copyright (c) 2014-2023 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.utils;
  22. import android.Manifest;
  23. import android.annotation.SuppressLint;
  24. import android.app.Activity;
  25. import android.app.ActivityManager;
  26. import android.app.AlarmManager;
  27. import android.app.Notification;
  28. import android.app.NotificationManager;
  29. import android.app.PendingIntent;
  30. import android.content.ContentProviderOperation;
  31. import android.content.ContentResolver;
  32. import android.content.Context;
  33. import android.content.Intent;
  34. import android.content.OperationApplicationException;
  35. import android.content.SharedPreferences;
  36. import android.content.pm.ActivityInfo;
  37. import android.content.pm.PackageInfo;
  38. import android.content.pm.PackageManager;
  39. import android.content.res.Configuration;
  40. import android.content.res.Resources;
  41. import android.content.res.TypedArray;
  42. import android.graphics.Color;
  43. import android.graphics.ColorMatrixColorFilter;
  44. import android.graphics.PorterDuff;
  45. import android.graphics.drawable.Drawable;
  46. import android.net.Uri;
  47. import android.os.Build;
  48. import android.os.Bundle;
  49. import android.os.RemoteException;
  50. import android.provider.Settings;
  51. import android.text.TextUtils;
  52. import android.util.DisplayMetrics;
  53. import android.util.TypedValue;
  54. import android.view.Menu;
  55. import android.view.MenuItem;
  56. import android.view.SubMenu;
  57. import android.view.Surface;
  58. import android.view.View;
  59. import android.view.ViewGroup;
  60. import android.view.WindowManager;
  61. import android.widget.ImageView;
  62. import android.widget.Toast;
  63. import androidx.activity.result.ActivityResultLauncher;
  64. import androidx.annotation.AttrRes;
  65. import androidx.annotation.ColorInt;
  66. import androidx.annotation.DrawableRes;
  67. import androidx.annotation.IntDef;
  68. import androidx.annotation.NonNull;
  69. import androidx.annotation.Nullable;
  70. import androidx.annotation.RequiresApi;
  71. import androidx.annotation.StringRes;
  72. import androidx.appcompat.app.AppCompatActivity;
  73. import androidx.appcompat.content.res.AppCompatResources;
  74. import androidx.appcompat.view.menu.MenuBuilder;
  75. import androidx.appcompat.widget.Toolbar;
  76. import androidx.core.app.ActivityCompat;
  77. import androidx.core.app.NotificationCompat;
  78. import androidx.core.content.ContextCompat;
  79. import androidx.core.view.MenuCompat;
  80. import androidx.fragment.app.Fragment;
  81. import androidx.preference.Preference;
  82. import androidx.preference.PreferenceGroup;
  83. import androidx.preference.PreferenceManager;
  84. import com.datatheorem.android.trustkit.TrustKit;
  85. import com.google.android.material.snackbar.BaseTransientBottomBar;
  86. import com.google.android.material.snackbar.Snackbar;
  87. import org.slf4j.Logger;
  88. import java.lang.annotation.Retention;
  89. import java.lang.annotation.RetentionPolicy;
  90. import java.lang.reflect.Method;
  91. import java.util.ArrayList;
  92. import java.util.List;
  93. import java.util.Locale;
  94. import javax.net.ssl.HttpsURLConnection;
  95. import javax.net.ssl.SSLSocketFactory;
  96. import ch.threema.app.BuildConfig;
  97. import ch.threema.app.BuildFlavor;
  98. import ch.threema.app.R;
  99. import ch.threema.app.ThreemaApplication;
  100. import ch.threema.app.activities.HomeActivity;
  101. import ch.threema.app.backuprestore.csv.BackupService;
  102. import ch.threema.app.backuprestore.csv.RestoreService;
  103. import ch.threema.app.dialogs.SimpleStringAlertDialog;
  104. import ch.threema.app.exceptions.FileSystemNotPresentException;
  105. import ch.threema.app.managers.ServiceManager;
  106. import ch.threema.app.notifications.NotificationBuilderWrapper;
  107. import ch.threema.app.services.AppRestrictionService;
  108. import ch.threema.app.services.LockAppService;
  109. import ch.threema.app.services.PreferenceService;
  110. import ch.threema.app.services.license.LicenseService;
  111. import ch.threema.app.threemasafe.ThreemaSafeConfigureActivity;
  112. import ch.threema.base.utils.LoggingUtil;
  113. import static android.content.res.Configuration.UI_MODE_NIGHT_YES;
  114. import static android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
  115. import static android.view.View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
  116. import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
  117. import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
  118. import static ch.threema.app.ThreemaApplication.getAppContext;
  119. import static ch.threema.app.camera.CameraUtil.isInternalCameraSupported;
  120. import static ch.threema.app.services.NotificationService.NOTIFICATION_CHANNEL_ALERT;
  121. import static ch.threema.app.services.NotificationServiceImpl.APP_RESTART_NOTIFICATION_ID;
  122. import static ch.threema.app.utils.IntentDataUtil.PENDING_INTENT_FLAG_MUTABLE;
  123. public class ConfigUtils {
  124. private static final Logger logger = LoggingUtil.getThreemaLogger("ConfigUtils");
  125. public static final int THEME_LIGHT = 0;
  126. public static final int THEME_DARK = 1;
  127. public static final int THEME_SYSTEM = 2;
  128. public static final int THEME_NONE = -1;
  129. private static final int CONTENT_PROVIDER_BATCH_SIZE = 50;
  130. @Retention(RetentionPolicy.SOURCE)
  131. @IntDef({THEME_LIGHT, THEME_DARK})
  132. public @interface AppTheme {}
  133. public static final int EMOJI_DEFAULT = 0;
  134. public static final int EMOJI_ANDROID = 1;
  135. private static int appTheme = THEME_NONE;
  136. private static String localeOverride = null;
  137. private static Integer primaryColor = null, accentColor = null, miuiVersion = null;
  138. private static int emojiStyle = 0;
  139. private static Boolean isTablet = null, isBiggerSingleEmojis = null, hasMapLibreSupport = null;
  140. private static int preferredThumbnailWidth = -1, preferredAudioMessageWidth = -1;
  141. private static final float[] NEGATIVE_MATRIX = {
  142. -1.0f, 0, 0, 0, 255, // red
  143. 0, -1.0f, 0, 0, 255, // green
  144. 0, 0, -1.0f, 0, 255, // blue
  145. 0, 0, 0, 1.0f, 0 // alpha
  146. };
  147. public static boolean isTabletLayout(Context context) {
  148. if (isTablet != null) {
  149. return isTablet;
  150. }
  151. isTablet = false;
  152. if (context != null) {
  153. Resources res = context.getResources();
  154. if (res != null) {
  155. isTablet = res.getBoolean(R.bool.tablet_layout);
  156. }
  157. }
  158. return isTablet;
  159. }
  160. public static boolean isTabletLayout() {
  161. Context appContext = ThreemaApplication.getAppContext();
  162. return isTabletLayout(appContext);
  163. }
  164. public static boolean isLandscape(Context context) {
  165. return context.getResources().getBoolean(R.bool.is_landscape);
  166. }
  167. public static boolean isBlackBerry() {
  168. String osName = System.getProperty("os.name");
  169. return osName != null && osName.equalsIgnoreCase("qnx");
  170. }
  171. public static boolean isAmazonDevice() {
  172. return (Build.MANUFACTURER.equals("Amazon"));
  173. }
  174. public static boolean isHuaweiDevice() {
  175. return (Build.MANUFACTURER.equalsIgnoreCase("Huawei") && !Build.MODEL.contains("Nexus"));
  176. }
  177. public static boolean isOnePlusDevice() {
  178. return (Build.MANUFACTURER.equalsIgnoreCase("OnePlus"));
  179. }
  180. public static boolean isSamsungDevice() {
  181. return (Build.MANUFACTURER.equalsIgnoreCase("Samsung"));
  182. }
  183. public static boolean isSonyDevice() {
  184. return (Build.MANUFACTURER.equalsIgnoreCase("Sony"));
  185. }
  186. public static boolean isNokiaDevice() {
  187. return Build.MANUFACTURER.equalsIgnoreCase("HMD Global");
  188. }
  189. public static boolean canDoGroupedNotifications() {
  190. return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
  191. }
  192. public static boolean supportsNotificationChannels() {
  193. return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
  194. }
  195. public static boolean supportsVideoCapture() {
  196. return isInternalCameraSupported();
  197. }
  198. public static boolean supportsPictureInPicture(Context context) {
  199. return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
  200. }
  201. public static boolean hasAsyncMediaCodecBug() {
  202. return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ConfigUtils.isSamsungDevice() && Build.MODEL.startsWith("SM-G97");
  203. }
  204. public static boolean hasScopedStorage() {
  205. return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
  206. }
  207. public static boolean isCallsEnabled() {
  208. ServiceManager serviceManager = ThreemaApplication.getServiceManager();
  209. if (serviceManager != null && serviceManager.getPreferenceService() != null) {
  210. return serviceManager.getPreferenceService().isVoipEnabled() &&
  211. !AppRestrictionUtil.isCallsDisabled();
  212. }
  213. return true;
  214. }
  215. public static boolean isVideoCallsEnabled() {
  216. ServiceManager serviceManager = ThreemaApplication.getServiceManager();
  217. if (serviceManager != null && serviceManager.getPreferenceService() != null) {
  218. return (BuildConfig.VIDEO_CALLS_ENABLED &&
  219. serviceManager.getPreferenceService().isVideoCallsEnabled() &&
  220. !AppRestrictionUtil.isVideoCallsDisabled());
  221. }
  222. return BuildConfig.VIDEO_CALLS_ENABLED;
  223. }
  224. public static boolean isGroupCallsEnabled() {
  225. ServiceManager serviceManager = ThreemaApplication.getServiceManager();
  226. if (serviceManager != null && serviceManager.getPreferenceService() != null) {
  227. return (BuildConfig.GROUP_CALLS_ENABLED &&
  228. serviceManager.getPreferenceService().isGroupCallsEnabled() &&
  229. !AppRestrictionUtil.isGroupCallsDisabled() &&
  230. !AppRestrictionUtil.isCallsDisabled());
  231. }
  232. return BuildConfig.GROUP_CALLS_ENABLED;
  233. }
  234. public static boolean isWorkDirectoryEnabled() {
  235. ServiceManager serviceManager = ThreemaApplication.getServiceManager();
  236. if (serviceManager != null && serviceManager.getPreferenceService() != null) {
  237. return (serviceManager.getPreferenceService().getWorkDirectoryEnabled() &&
  238. !AppRestrictionUtil.isWorkDirectoryDisabled());
  239. }
  240. return false;
  241. }
  242. /**
  243. * Get a Socket Factory for certificate pinning and forced TLS version upgrade.
  244. */
  245. public static @NonNull SSLSocketFactory getSSLSocketFactory(String host) {
  246. return new TLSUpgradeSocketFactoryWrapper(
  247. ConfigUtils.isOnPremBuild() ?
  248. HttpsURLConnection.getDefaultSSLSocketFactory() :
  249. TrustKit.getInstance().getSSLSocketFactory(host));
  250. }
  251. public static boolean hasNoMapLibreSupport() {
  252. /* Some broken Samsung devices crash on MapLibre initialization due to a compiler bug, see https://issuetracker.google.com/issues/37013676 */
  253. /* Device that do not support OCSP stapling cannot use our maps and POI servers */
  254. if (hasMapLibreSupport == null) {
  255. hasMapLibreSupport =
  256. Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1 && Build.MANUFACTURER.equalsIgnoreCase("marshall");
  257. }
  258. return hasMapLibreSupport;
  259. }
  260. public static boolean isXiaomiDevice() {
  261. return Build.MANUFACTURER.equalsIgnoreCase("Xiaomi");
  262. }
  263. /**
  264. * return current MIUI version level or 0 if no Xiaomi device or MIUI version is not recognized or not relevant
  265. * @return MIUI version level or 0
  266. */
  267. public static int getMIUIVersion() {
  268. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isXiaomiDevice()) {
  269. return 0;
  270. }
  271. if (miuiVersion == null) {
  272. miuiVersion = 0;
  273. try {
  274. Class<?> c = Class.forName("android.os.SystemProperties");
  275. Method get = c.getMethod("get", String.class);
  276. String version = (String) get.invoke(c, "ro.miui.ui.version.name");
  277. if (version != null) {
  278. if (version.startsWith("V10")) {
  279. miuiVersion = 10;
  280. } else if (version.startsWith("V11")) {
  281. miuiVersion = 11;
  282. } else if (version.startsWith("V12") || version.startsWith("V13")) {
  283. miuiVersion = 12;
  284. }
  285. }
  286. } catch (Exception ignored) { }
  287. }
  288. return miuiVersion;
  289. }
  290. public static int getAppTheme(Context context) {
  291. if (appTheme == THEME_NONE) {
  292. appTheme = Integer.valueOf(PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.preferences__theme), "2"));
  293. }
  294. if (appTheme == THEME_SYSTEM) {
  295. appTheme = (context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES ? THEME_DARK : THEME_LIGHT;
  296. }
  297. return appTheme;
  298. }
  299. public static void setAppTheme(int theme) {
  300. appTheme = theme;
  301. primaryColor = null;
  302. }
  303. public static void resetAppTheme() {
  304. appTheme = THEME_NONE;
  305. primaryColor = null;
  306. }
  307. private static void setPrimaryColor(Context context) {
  308. if (primaryColor == null) {
  309. primaryColor = getColorFromAttribute(context, R.attr.textColorPrimary);
  310. }
  311. }
  312. public static @ColorInt int getColorFromAttribute(Context context, @AttrRes int attr) {
  313. TypedArray typedArray = context.getTheme().obtainStyledAttributes(new int[] { attr });
  314. @ColorInt int color = typedArray.getColor(0, -1);
  315. typedArray.recycle();
  316. return color;
  317. }
  318. public static @ColorInt int getPrimaryColor() {
  319. return primaryColor != null ? primaryColor : 0xFFFFFFFF;
  320. }
  321. public static Drawable getThemedDrawable(Context context, @DrawableRes int resId) {
  322. Drawable drawable = AppCompatResources.getDrawable(context, resId);
  323. if (drawable != null) {
  324. if (appTheme != THEME_LIGHT) {
  325. setPrimaryColor(context);
  326. drawable.setColorFilter(primaryColor, PorterDuff.Mode.SRC_IN);
  327. return drawable;
  328. } else {
  329. drawable.clearColorFilter();
  330. }
  331. }
  332. return drawable;
  333. }
  334. public static void themeImageView(Context context, ImageView view) {
  335. if (appTheme != THEME_LIGHT) {
  336. if (context == null) {
  337. return;
  338. }
  339. setPrimaryColor(context);
  340. view.setColorFilter(primaryColor, PorterDuff.Mode.SRC_IN);
  341. } else {
  342. view.clearColorFilter();
  343. }
  344. }
  345. public static void themeMenu(Menu menu, @ColorInt int color) {
  346. for (int i = 0, size = menu.size(); i < size; i++) {
  347. final MenuItem menuItem = menu.getItem(i);
  348. themeMenuItem(menuItem, color);
  349. if (menuItem.hasSubMenu()) {
  350. final SubMenu subMenu = menuItem.getSubMenu();
  351. for (int j = 0; j < subMenu.size(); j++) {
  352. themeMenuItem(subMenu.getItem(j), color);
  353. }
  354. }
  355. }
  356. }
  357. public static void themeMenuItem(final MenuItem menuItem, @ColorInt int color) {
  358. if (menuItem != null) {
  359. final Drawable drawable = menuItem.getIcon();
  360. if (drawable != null) {
  361. drawable.mutate();
  362. drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
  363. }
  364. }
  365. }
  366. public static void setEmojiStyle(Context context, int newStyle) {
  367. if (newStyle != -1) {
  368. emojiStyle = newStyle;
  369. } else {
  370. if (BuildFlavor.isLibre()) {
  371. emojiStyle = EMOJI_ANDROID;
  372. return;
  373. }
  374. emojiStyle = Integer.valueOf(
  375. PreferenceManager.getDefaultSharedPreferences(context).
  376. getString(context.getString(R.string.preferences__emoji_style),
  377. "0"));
  378. }
  379. }
  380. public static boolean isBiggerSingleEmojis(Context context) {
  381. if (isBiggerSingleEmojis == null) {
  382. isBiggerSingleEmojis = PreferenceManager.getDefaultSharedPreferences(context).getBoolean(context.getString(R.string.preferences__bigger_single_emojis), true);
  383. }
  384. return isBiggerSingleEmojis;
  385. }
  386. public static void setBiggerSingleEmojis(boolean value) {
  387. isBiggerSingleEmojis = value;
  388. }
  389. public static boolean isDefaultEmojiStyle() {
  390. return emojiStyle == EMOJI_DEFAULT;
  391. }
  392. /**
  393. * Get user-facing application version string including alpha/beta version suffix
  394. *
  395. * @return application version string
  396. */
  397. public static String getAppVersion(@NonNull Context context) {
  398. try {
  399. PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
  400. if (packageInfo != null) {
  401. return packageInfo.versionName;
  402. }
  403. } catch (PackageManager.NameNotFoundException e) {
  404. logger.error("Exception", e);
  405. }
  406. return "";
  407. }
  408. /**
  409. * Get user-facing application version represented as a float value stripping any non-numeric characters such as suffixes for build type (e.g. 4.0f)
  410. * @param context
  411. * @return version number
  412. */
  413. public static float getAppVersionFloat(@NonNull Context context) {
  414. try {
  415. String versionString = ConfigUtils.getAppVersion(context).replaceAll("[^\\d.]", "");
  416. return Float.parseFloat(versionString);
  417. } catch (NumberFormatException e) {
  418. logger.error("Exception", e);
  419. }
  420. return 1.0f;
  421. }
  422. /**
  423. * Get build number of this app build
  424. * @param context
  425. * @return build number
  426. */
  427. public static int getBuildNumber(Context context) {
  428. try {
  429. PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
  430. if (packageInfo != null) {
  431. return packageInfo.versionCode;
  432. }
  433. } catch (PackageManager.NameNotFoundException x) {
  434. logger.error("Exception", x);
  435. }
  436. return 0;
  437. }
  438. /**
  439. * Return information about the device, including the manufacturer and the model.
  440. *
  441. * @param context The Android context.
  442. */
  443. public static @NonNull String getDeviceInfo(Context context, boolean includeAppVersion) {
  444. final StringBuilder info = new StringBuilder();
  445. if (includeAppVersion) {
  446. info.append(getAppVersion(context)).append("/");
  447. }
  448. info.append(Build.MANUFACTURER).append(";")
  449. .append(Build.MODEL).append("/")
  450. .append(Build.VERSION.RELEASE).append("/")
  451. .append(BuildFlavor.getName());
  452. return info.toString();
  453. }
  454. /**
  455. * Return information about the device including the manufacturer and the model.
  456. * The version is NOT included.
  457. * If mdm parameters are active on this device they are also appended according to ANDR-2213.
  458. *
  459. * @return The device info meant to be sent with support requests
  460. */
  461. public static @NonNull String getSupportDeviceInfo(Context context) {
  462. final StringBuilder info = new StringBuilder(getDeviceInfo(context, false));
  463. if (isWorkRestricted()) {
  464. String mdmSource = AppRestrictionService.getInstance().getMdmSource();
  465. if (mdmSource != null) {
  466. info.append("/").append(mdmSource);
  467. }
  468. }
  469. return info.toString();
  470. }
  471. public static String getPrivacyPolicyURL(Context context) {
  472. return getLicenceURL(context, R.string.privacy_policy_url);
  473. }
  474. public static String getTermsOfServiceURL(Context context) {
  475. return getLicenceURL(context, R.string.terms_of_service_url);
  476. }
  477. public static String getEulaURL(Context context) {
  478. return getLicenceURL(context, R.string.eula_url);
  479. }
  480. private static String getLicenceURL(Context context, @StringRes int url) {
  481. String lang = LocaleUtil.getAppLanguage().startsWith("de") ? "de" : "en";
  482. String version = ConfigUtils.getAppVersion(context);
  483. String theme = ConfigUtils.getAppTheme(context) == ConfigUtils.THEME_DARK ? "dark" : "light";
  484. return String.format(context.getString(url), lang, version, theme);
  485. }
  486. public static String getWorkExplainURL(Context context) {
  487. String lang = LocaleUtil.getAppLanguage();
  488. if (lang.length() >= 2) {
  489. lang = lang.substring(0, 2);
  490. } else {
  491. lang = "en";
  492. }
  493. return String.format(context.getString(R.string.work_explain_url), lang);
  494. }
  495. public static void recreateActivity(Activity activity) {
  496. activity.finish();
  497. final Intent intent = new Intent(activity, HomeActivity.class);
  498. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
  499. activity.startActivity(intent);
  500. }
  501. public static void recreateActivity(Activity activity, Class<?> cls, Bundle bundle) {
  502. activity.finish();
  503. final Intent intent = new Intent(activity, cls);
  504. if (bundle != null) {
  505. intent.putExtras(bundle);
  506. }
  507. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
  508. activity.startActivity(intent);
  509. }
  510. public static void scheduleAppRestart(Context context, int delayMs, String eventTriggerTitle) {
  511. // Android Q does not allow restart in the background
  512. // https://developer.android.com/preview/privacy/background-activity-starts
  513. Intent restartIntent = context.getPackageManager()
  514. .getLaunchIntentForPackage(context.getPackageName());
  515. restartIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
  516. PendingIntent pendingIntent = PendingIntent.getActivity(
  517. context, 0,
  518. restartIntent, PendingIntent.FLAG_CANCEL_CURRENT | PENDING_INTENT_FLAG_MUTABLE);
  519. if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
  520. // on older android version we restart directly after delayMs
  521. AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
  522. manager.set(AlarmManager.RTC, System.currentTimeMillis() + delayMs, pendingIntent);
  523. } else {
  524. String text = context.getString(R.string.tap_to_start, context.getString(R.string.app_name));
  525. NotificationCompat.Builder builder =
  526. new NotificationBuilderWrapper(context, NOTIFICATION_CHANNEL_ALERT, null)
  527. .setSmallIcon(R.drawable.ic_notification_small)
  528. .setContentTitle(eventTriggerTitle)
  529. .setContentText(eventTriggerTitle)
  530. .setDefaults(Notification.DEFAULT_LIGHTS | Notification.DEFAULT_VIBRATE)
  531. .setColor(context.getResources().getColor(R.color.material_green))
  532. .setPriority(NotificationCompat.PRIORITY_MAX)
  533. .setStyle(new NotificationCompat.BigTextStyle().bigText(text))
  534. .setContentIntent(pendingIntent)
  535. .setAutoCancel(false);
  536. NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
  537. if (notificationManager != null) {
  538. notificationManager.notify(APP_RESTART_NOTIFICATION_ID, builder.build());
  539. }
  540. }
  541. }
  542. public static boolean checkAvailableMemory(float required) {
  543. // Get max available VM memory, exceeding this amount will throw an
  544. // OutOfMemory exception
  545. return (Runtime.getRuntime().maxMemory() > required);
  546. }
  547. public static boolean isWorkBuild() {
  548. return BuildFlavor.getLicenseType().equals(BuildFlavor.LicenseType.GOOGLE_WORK)
  549. || BuildFlavor.getLicenseType().equals(BuildFlavor.LicenseType.HMS_WORK)
  550. || isOnPremBuild();
  551. }
  552. public static boolean isOnPremBuild() {
  553. return BuildFlavor.getLicenseType().equals(BuildFlavor.LicenseType.ONPREM);
  554. }
  555. public static boolean isDemoOPServer(@NonNull PreferenceService preferenceService) {
  556. return preferenceService.getOnPremServer() != null && preferenceService.getOnPremServer().toLowerCase().contains(".3ma.ch/");
  557. }
  558. public static boolean isTestBuild() {
  559. return BuildFlavor.getName().contains("DEBUG") ||
  560. BuildFlavor.getName().equals("Red") || BuildFlavor.getName().equals("DEV") ||
  561. BuildFlavor.getName().equals("Sandbox");
  562. }
  563. public static boolean supportsGroupLinks() {
  564. return false;
  565. }
  566. public static boolean supportGroupDescription() {
  567. return false;
  568. }
  569. /**
  570. * Returns true if this is a work build and app is under control of a device policy controller (DPC) or Threema MDM
  571. * @return boolean
  572. */
  573. public static boolean isWorkRestricted() {
  574. if (!isWorkBuild()) {
  575. return false;
  576. }
  577. Bundle restrictions = AppRestrictionService.getInstance()
  578. .getAppRestrictions();
  579. return restrictions != null && !restrictions.isEmpty();
  580. }
  581. public static boolean isSerialLicenseValid() {
  582. ServiceManager serviceManager = ThreemaApplication.getServiceManager();
  583. if (serviceManager != null) {
  584. if (isOnPremBuild()) {
  585. // OnPrem needs server info in addition to license
  586. if (serviceManager.getPreferenceService().getOnPremServer() == null) {
  587. return false;
  588. }
  589. }
  590. try {
  591. LicenseService licenseService = serviceManager.getLicenseService();
  592. if (licenseService != null) {
  593. return isSerialLicensed() && licenseService.hasCredentials() && licenseService.isLicensed();
  594. }
  595. } catch (FileSystemNotPresentException e) {
  596. logger.error("Exception", e);
  597. }
  598. }
  599. return false;
  600. }
  601. public static boolean isSerialLicensed() {
  602. return BuildFlavor.getLicenseType().equals(BuildFlavor.LicenseType.GOOGLE_WORK)
  603. || BuildFlavor.getLicenseType().equals(BuildFlavor.LicenseType.HMS_WORK)
  604. || BuildFlavor.getLicenseType().equals(BuildFlavor.LicenseType.SERIAL)
  605. || BuildFlavor.getLicenseType().equals(BuildFlavor.LicenseType.ONPREM);
  606. }
  607. public static boolean hasInvalidCredentials() {
  608. return (ConfigUtils.isOnPremBuild() || ConfigUtils.isWorkBuild()) && ConfigUtils.isSerialLicensed() && !ConfigUtils.isSerialLicenseValid();
  609. }
  610. /**
  611. * Returns true if privacy settings imply that screenshots and app switcher thumbnails should be disabled
  612. * @param preferenceService
  613. * @param lockAppService
  614. * @return true if disabled, false otherwise or in case of failure
  615. */
  616. public static boolean getScreenshotsDisabled(@Nullable PreferenceService preferenceService, @Nullable LockAppService lockAppService) {
  617. return preferenceService != null && lockAppService != null && lockAppService.isLockingEnabled();
  618. }
  619. public static void setScreenshotsAllowed(@NonNull Activity activity, @Nullable PreferenceService preferenceService, @Nullable LockAppService lockAppService) {
  620. // call this before setContentView
  621. if (getScreenshotsDisabled(preferenceService, lockAppService) ||
  622. (preferenceService != null && preferenceService.isDisableScreenshots()) ||
  623. activity instanceof ThreemaSafeConfigureActivity) {
  624. activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
  625. } else {
  626. activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
  627. }
  628. }
  629. public static @ColorInt int getAccentColor(Context context) {
  630. if (accentColor == null) {
  631. resetAccentColor(context);
  632. }
  633. return accentColor;
  634. }
  635. public static void resetAccentColor(Context context) {
  636. TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{R.attr.colorAccent});
  637. accentColor = a.getColor(0, 0);
  638. a.recycle();
  639. }
  640. @Deprecated
  641. public static boolean useContentUris() {
  642. return true;
  643. }
  644. public static boolean hasProtection(PreferenceService preferenceService) {
  645. return !PreferenceService.LockingMech_NONE.equals(preferenceService.getLockMechanism());
  646. }
  647. public static void setLocaleOverride(@Nullable Context context, @Nullable PreferenceService preferenceService) {
  648. if (context == null) {
  649. return;
  650. }
  651. if (preferenceService == null) {
  652. return;
  653. }
  654. if (localeOverride == null) {
  655. String localeString = preferenceService.getLocaleOverride();
  656. localeOverride = localeString != null ? localeString : "";
  657. }
  658. try {
  659. Resources res = context.getResources();
  660. String systemLanguage = Resources.getSystem().getConfiguration().locale.getLanguage();
  661. String confLanguage = res.getConfiguration().locale.getLanguage();
  662. if (localeOverride.isEmpty()) {
  663. if (systemLanguage.equals(confLanguage)) {
  664. return;
  665. } else {
  666. confLanguage = systemLanguage;
  667. }
  668. } else {
  669. confLanguage = localeOverride;
  670. }
  671. DisplayMetrics dm = res.getDisplayMetrics();
  672. android.content.res.Configuration conf = res.getConfiguration();
  673. switch (confLanguage) {
  674. case "pt":
  675. conf.locale = new Locale(confLanguage, "BR");
  676. break;
  677. case "zh-rCN":
  678. conf.locale = new Locale("zh", "CN");
  679. break;
  680. case "zh-rTW":
  681. conf.locale = new Locale("zh", "TW");
  682. break;
  683. case "be-rBY":
  684. conf.locale = new Locale("be", "BY");
  685. break;
  686. default:
  687. conf.locale = new Locale(confLanguage);
  688. break;
  689. }
  690. res.updateConfiguration(conf, dm);
  691. Locale.setDefault(conf.locale);
  692. } catch (Exception e) {
  693. //
  694. }
  695. }
  696. public static void updateLocaleOverride(Object newValue) {
  697. if (newValue != null) {
  698. String newLocale = newValue.toString();
  699. if (!TestUtil.empty(newLocale)) {
  700. localeOverride = newLocale;
  701. return;
  702. }
  703. }
  704. localeOverride = null;
  705. }
  706. /*
  707. * Update the app locale to avoid having to restart if relying on the app context to get resources
  708. */
  709. public static void updateAppContextLocale(Context context, String lang) {
  710. Configuration config = new Configuration();
  711. if (!TextUtils.isEmpty(lang)) {
  712. config.locale = new Locale(lang);
  713. } else {
  714. config.locale = Locale.getDefault();
  715. }
  716. context.getResources().updateConfiguration(config, null);
  717. }
  718. /*
  719. * Returns the height of the status bar (showing battery or network status) on top of the screen
  720. * DEPRECATED: use ViewCompat.setOnApplyWindowInsetsListener() on Lollipop+
  721. */
  722. @Deprecated
  723. public static int getStatusBarHeight(Context context) {
  724. int result = 0;
  725. int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
  726. if (resourceId > 0) {
  727. result = context.getResources().getDimensionPixelSize(resourceId);
  728. }
  729. return result;
  730. }
  731. /*
  732. * Returns the height of the navigation softkey bar at the bottom of some devices
  733. * DEPRECATED: use ViewCompat.setOnApplyWindowInsetsListener() on Lollipop+
  734. */
  735. @Deprecated
  736. public static int getNavigationBarHeight(Activity activity) {
  737. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode()) {
  738. return 0;
  739. }
  740. NavigationBarDimensions dimensions = new NavigationBarDimensions();
  741. dimensions = getNavigationBarDimensions(activity.getWindowManager(), dimensions);
  742. return dimensions.height;
  743. }
  744. @Deprecated
  745. public static NavigationBarDimensions getNavigationBarDimensions(WindowManager windowManager, NavigationBarDimensions dimensions) {
  746. dimensions.width = dimensions.height = 0;
  747. DisplayMetrics metrics = new DisplayMetrics();
  748. // get dimensions of usable display space with decorations (status bar / navigation bar) subtracted
  749. windowManager.getDefaultDisplay().getMetrics(metrics);
  750. int usableHeight = metrics.heightPixels;
  751. int usableWidth = metrics.widthPixels;
  752. // get dimensions of display without subtracting any decorations
  753. windowManager.getDefaultDisplay().getRealMetrics(metrics);
  754. int realHeight = metrics.heightPixels;
  755. int realWidth = metrics.widthPixels;
  756. if (realHeight > usableHeight)
  757. dimensions.height = realHeight - usableHeight;
  758. if (realWidth > usableWidth)
  759. dimensions.width = realWidth - usableWidth;
  760. return dimensions;
  761. }
  762. public static int getUsableWidth(WindowManager windowManager) {
  763. DisplayMetrics metrics = new DisplayMetrics();
  764. windowManager.getDefaultDisplay().getMetrics(metrics);
  765. return metrics.widthPixels;
  766. }
  767. public static boolean checkManifestPermission(Context context, String packageName, final String permission) {
  768. if (TextUtils.isEmpty(permission)) {
  769. return false;
  770. }
  771. PackageManager pm = context.getPackageManager();
  772. try {
  773. PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
  774. if (packageInfo != null) {
  775. String[] requestedPermissions = packageInfo.requestedPermissions;
  776. if (requestedPermissions != null && requestedPermissions.length > 0) {
  777. for (String requestedPermission : requestedPermissions) {
  778. if (permission.equalsIgnoreCase(requestedPermission)) {
  779. return true;
  780. }
  781. }
  782. }
  783. }
  784. }
  785. catch (PackageManager.NameNotFoundException e) {
  786. logger.error("Exception", e);
  787. }
  788. return false;
  789. }
  790. public static class NavigationBarDimensions {
  791. public int width;
  792. public int height;
  793. }
  794. public static int getActionBarSize(Context context) {
  795. TypedValue tv = new TypedValue();
  796. if (context.getTheme().resolveAttribute(R.attr.actionBarSize, tv, true)) {
  797. return TypedValue.complexToDimensionPixelSize(tv.data, context.getResources().getDisplayMetrics());
  798. }
  799. return 0;
  800. }
  801. public static void adjustToolbar(Context context, Toolbar toolbar) {
  802. // adjust toolbar height after rotate
  803. if (toolbar != null) {
  804. int size = getActionBarSize(context);
  805. toolbar.setMinimumHeight(size);
  806. ViewGroup.LayoutParams lp = toolbar.getLayoutParams();
  807. lp.height = size;
  808. toolbar.setLayoutParams(lp);
  809. }
  810. }
  811. public static void invertColors(ImageView imageView) {
  812. imageView.setColorFilter(new ColorMatrixColorFilter(NEGATIVE_MATRIX));
  813. }
  814. public static boolean isPermissionGranted(@NonNull Context context, @NonNull String permission) {
  815. return Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
  816. ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED;
  817. }
  818. /**
  819. * Request all possibly required permissions of Contacts group
  820. * @param activity Activity context for onRequestPermissionsResult callback
  821. * @param requestCode request code for onRequestPermissionsResult callback
  822. * @return true if permissions are already granted, false otherwise
  823. */
  824. public static boolean requestContactPermissions(@NonNull Activity activity, Fragment fragment, int requestCode) {
  825. String[] permissions = new String[]{Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS, Manifest.permission.GET_ACCOUNTS};
  826. if (checkIfNeedsPermissionRequest(activity, permissions)) {
  827. requestPermissions(activity, fragment, permissions, requestCode);
  828. return false;
  829. }
  830. return true;
  831. }
  832. /**
  833. * Request all possibly required permissions of Storage group
  834. * @param activity Activity context for onRequestPermissionsResult callback
  835. * @param requestCode request code for onRequestPermissionsResult callback
  836. * @return true if permissions are already granted, false otherwise
  837. */
  838. public static boolean requestStoragePermissions(@NonNull Activity activity, Fragment fragment, int requestCode) {
  839. String[] permissions = new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE};
  840. if (checkIfNeedsPermissionRequest(activity, permissions)) {
  841. requestPermissions(activity, fragment, permissions, requestCode);
  842. return false;
  843. }
  844. return true;
  845. }
  846. /**
  847. * Request storage write permission
  848. * @param activity Activity context for onRequestPermissionsResult callback
  849. * @param requestCode request code for onRequestPermissionsResult callback
  850. * @return true if permissions are already granted, false otherwise
  851. */
  852. public static boolean requestWriteStoragePermissions(@NonNull Activity activity, Fragment fragment, int requestCode) {
  853. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
  854. // scoped storage
  855. return true;
  856. }
  857. String[] permissions = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
  858. if (checkIfNeedsPermissionRequest(activity, permissions)) {
  859. requestPermissions(activity, fragment, permissions, requestCode);
  860. return false;
  861. }
  862. return true;
  863. }
  864. /**
  865. * Request all possibly required permissions of Location group
  866. * @param activity Activity context for onRequestPermissionsResult callback
  867. * @param requestCode request code for onRequestPermissionsResult callback
  868. * @return true if permissions are already granted, false otherwise
  869. */
  870. public static boolean requestLocationPermissions(@NonNull Activity activity, Fragment fragment, int requestCode) {
  871. String[] permissions = new String[]{Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION};
  872. if (checkIfNeedsPermissionRequest(activity, permissions)) {
  873. requestPermissions(activity, fragment, permissions, requestCode);
  874. return false;
  875. }
  876. return true;
  877. }
  878. /**
  879. * Asynchronously request audio permissions.
  880. *
  881. * @param activity Activity context for onRequestPermissionsResult callback
  882. * @param requestCode request code for onRequestPermissionsResult callback
  883. * @return true if permissions are already granted, false otherwise
  884. */
  885. public static boolean requestAudioPermissions(@NonNull Activity activity, Fragment fragment, int requestCode) {
  886. final String[] permissions = new String[]{ Manifest.permission.RECORD_AUDIO };
  887. if (checkIfNeedsPermissionRequest(activity, permissions)) {
  888. requestPermissions(activity, fragment, permissions, requestCode);
  889. return false;
  890. }
  891. return true;
  892. }
  893. /**
  894. * Asynchronously request permission required for checking for connected bluetooth devices in Android S
  895. *
  896. * @param activity Activity context for onRequestPermissionsResult callback
  897. * @param requestCode request code for onRequestPermissionsResult callback
  898. * @return true if permissions are already granted, false otherwise
  899. */
  900. @RequiresApi(api = Build.VERSION_CODES.S)
  901. public static boolean requestBluetoothConnectPermissions(@NonNull AppCompatActivity activity, Fragment fragment, int requestCode, boolean showHelpDialog) {
  902. final String[] permissions = new String[]{ Manifest.permission.BLUETOOTH_CONNECT };
  903. if (checkIfNeedsPermissionRequest(activity, permissions)) {
  904. if (showHelpDialog) {
  905. SimpleStringAlertDialog.newInstance(R.string.voip_bluetooth, R.string.permission_bluetooth_connect_required)
  906. .setOnDismissRunnable(() -> requestPermissions(activity, fragment, permissions, requestCode))
  907. .show(activity.getSupportFragmentManager(), "");
  908. } else {
  909. requestPermissions(activity, fragment, permissions, requestCode);
  910. }
  911. return false;
  912. }
  913. return true;
  914. }
  915. /**
  916. * Request all possibly required permissions of Phone group
  917. * @param activity Activity context for onRequestPermissionsResult callback
  918. * @param fragment Fragment context for onRequestPermissionsResult callback
  919. * @param requestCode request code for onRequestPermissionsResult callback
  920. * @return true if permissions are already granted, false otherwise
  921. */
  922. public static boolean requestPhonePermissions(@NonNull Activity activity, Fragment fragment, int requestCode) {
  923. String[] permissions;
  924. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  925. permissions = new String[]{Manifest.permission.READ_PHONE_STATE, Manifest.permission.CALL_PHONE, Manifest.permission.ANSWER_PHONE_CALLS};
  926. } else {
  927. permissions = new String[]{Manifest.permission.READ_PHONE_STATE, Manifest.permission.CALL_PHONE};
  928. }
  929. if (checkIfNeedsPermissionRequest(activity, permissions)) {
  930. requestPermissions(activity, fragment, permissions, requestCode);
  931. return false;
  932. }
  933. return true;
  934. }
  935. /**
  936. * Request read phone state permission.
  937. *
  938. * @param activity activity context
  939. * @param requestPermissionLauncher the request permission launcher
  940. */
  941. public static void requestReadPhonePermission(@NonNull Activity activity,@Nullable ActivityResultLauncher<String> requestPermissionLauncher) {
  942. String permission = Manifest.permission.READ_PHONE_STATE;
  943. if (checkIfNeedsPermissionRequest(activity, new String[]{permission}) && requestPermissionLauncher != null) {
  944. requestPermissionLauncher.launch(permission);
  945. }
  946. }
  947. /**
  948. * Asynchronously request camera permissions.
  949. *
  950. * @param activity Activity context for onRequestPermissionsResult callback
  951. * @param fragment Fragment context for onRequestPermissionsResult callback
  952. * @param requestCode request code for onRequestPermissionsResult callback
  953. * @return true if permissions are already granted, false otherwise
  954. */
  955. public static boolean requestCameraPermissions(@NonNull Activity activity, Fragment fragment, int requestCode) {
  956. String[] permissions = new String[]{ Manifest.permission.CAMERA };
  957. if (checkIfNeedsPermissionRequest(activity, permissions)) {
  958. requestPermissions(activity, fragment, permissions, requestCode);
  959. return false;
  960. }
  961. return true;
  962. }
  963. private static void requestPermissions(Activity activity, Fragment fragment, String[] permissions, int requestCode) {
  964. if (fragment != null) {
  965. fragment.requestPermissions(permissions, requestCode);
  966. } else {
  967. ActivityCompat.requestPermissions(activity, permissions, requestCode);
  968. }
  969. }
  970. private static boolean checkIfNeedsPermissionRequest(@NonNull Context context, String[] permissions) {
  971. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  972. for (String permission : permissions) {
  973. if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
  974. return true;
  975. }
  976. }
  977. }
  978. return false;
  979. }
  980. /**
  981. * Show a snackbar explaining the reason why the user should enable a certain permission
  982. * @param context
  983. * @param parentLayout
  984. * @param stringResource
  985. */
  986. public static void showPermissionRationale(Context context, View parentLayout, @StringRes int stringResource) {
  987. showPermissionRationale(context, parentLayout, stringResource, null);
  988. }
  989. /**
  990. * Show a snackbar explaining the reason why the user should enable a certain permission
  991. * @param context
  992. * @param parentLayout
  993. * @param stringResource
  994. * @param callback Callback for the snackbar
  995. */
  996. public static void showPermissionRationale(
  997. Context context,
  998. @Nullable View parentLayout,
  999. @StringRes int stringResource,
  1000. @Nullable BaseTransientBottomBar.BaseCallback<Snackbar> callback
  1001. ) {
  1002. if (context == null) {
  1003. return;
  1004. }
  1005. if (parentLayout == null) {
  1006. Toast.makeText(context, context.getString(stringResource), Toast.LENGTH_LONG).show();
  1007. } else {
  1008. Snackbar snackbar = SnackbarUtil.make(parentLayout, context.getString(stringResource), Snackbar.LENGTH_LONG, 5);
  1009. snackbar.setAction(R.string.menu_settings, new View.OnClickListener() {
  1010. @Override
  1011. public void onClick(View v) {
  1012. Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
  1013. intent.setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID));
  1014. context.startActivity(intent);
  1015. }
  1016. });
  1017. if (callback != null) {
  1018. snackbar.addCallback(callback);
  1019. }
  1020. snackbar.show();
  1021. }
  1022. }
  1023. /**
  1024. * Configure activity and status bar based on user selected theme. Must be called before super.onCreate()
  1025. * @param activity
  1026. */
  1027. public static void configureActivityTheme(Activity activity) {
  1028. configureActivityTheme(activity, THEME_NONE);
  1029. }
  1030. public static void configureActivityTheme(Activity activity, int themeOverride) {
  1031. int orgTheme = 0;
  1032. try {
  1033. orgTheme = activity.getPackageManager().getActivityInfo(activity.getComponentName(), 0).theme;
  1034. } catch (Exception e) {
  1035. logger.error("Exception", e);
  1036. }
  1037. int desiredTheme = themeOverride == THEME_NONE ? getAppTheme(activity) : themeOverride;
  1038. if (desiredTheme == ConfigUtils.THEME_DARK) {
  1039. int newTheme;
  1040. switch (orgTheme) {
  1041. case R.style.AppBaseTheme:
  1042. newTheme = R.style.AppBaseTheme_Dark;
  1043. break;
  1044. case R.style.Theme_Threema_WithToolbarAndCheck:
  1045. newTheme = R.style.Theme_Threema_WithToolbarAndCheck_Dark;
  1046. break;
  1047. case R.style.Theme_Threema_TransparentStatusbar:
  1048. newTheme = R.style.Theme_Threema_TransparentStatusbar_Dark;
  1049. break;
  1050. case R.style.Theme_Threema_Translucent:
  1051. newTheme = R.style.Theme_Threema_Translucent_Dark;
  1052. break;
  1053. case R.style.Theme_Threema_VoiceRecorder:
  1054. newTheme = R.style.Theme_Threema_VoiceRecorder_Dark;
  1055. break;
  1056. case R.style.Theme_LocationPicker:
  1057. newTheme = R.style.Theme_LocationPicker_Dark;
  1058. break;
  1059. case R.style.Theme_MediaAttacher:
  1060. newTheme = R.style.Theme_MediaAttacher_Dark;
  1061. break;
  1062. case R.style.Theme_Threema_WhatsNew:
  1063. newTheme = R.style.Theme_Threema_WhatsNew_Dark;
  1064. break;
  1065. case R.style.Theme_Threema_WithToolbar_NoAnim:
  1066. newTheme = R.style.Theme_Threema_WithToolbar_NoAnim_Dark;
  1067. break;
  1068. case R.style.Theme_Threema_BiometricUnlock:
  1069. newTheme = R.style.Theme_Threema_BiometricUnlock_Dark;
  1070. break;
  1071. case R.style.Theme_Threema_NoActionBar:
  1072. case R.style.Theme_Threema_LowProfile:
  1073. case R.style.Theme_Threema_Transparent_Background:
  1074. case R.style.Theme_Threema_MediaViewer:
  1075. // agnostic themes: leave them alone
  1076. newTheme = orgTheme;
  1077. break;
  1078. default:
  1079. newTheme = R.style.Theme_Threema_WithToolbar_Dark;
  1080. break;
  1081. }
  1082. if (newTheme != orgTheme) {
  1083. activity.setTheme(newTheme);
  1084. }
  1085. if (orgTheme != R.style.Theme_Threema_TransparentStatusbar) {
  1086. activity.getWindow().addFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
  1087. activity.getWindow().setStatusBarColor(Color.BLACK);
  1088. if (Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
  1089. View decorView = activity.getWindow().getDecorView();
  1090. decorView.setSystemUiVisibility(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
  1091. }
  1092. }
  1093. } else {
  1094. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
  1095. activity.getWindow().setNavigationBarColor(Color.BLACK);
  1096. if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
  1097. activity.getWindow().setStatusBarColor(Color.BLACK);
  1098. } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
  1099. View decorView = activity.getWindow().getDecorView();
  1100. decorView.setSystemUiVisibility(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS | SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
  1101. }
  1102. } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O && (orgTheme != R.style.Theme_Threema_MediaViewer && orgTheme != R.style.Theme_Threema_Transparent_Background)) {
  1103. View decorView = activity.getWindow().getDecorView();
  1104. decorView.setSystemUiVisibility(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS |
  1105. SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR | SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
  1106. }
  1107. }
  1108. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
  1109. WindowManager.LayoutParams params = activity.getWindow().getAttributes();
  1110. params.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
  1111. }
  1112. }
  1113. public static void configureTransparentStatusBar(AppCompatActivity activity) {
  1114. activity.getWindow().setStatusBarColor(activity.getResources().getColor(R.color.status_bar_detail));
  1115. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  1116. activity.getWindow().getDecorView().setSystemUiVisibility(
  1117. activity.getWindow().getDecorView().getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
  1118. }
  1119. }
  1120. private static void tintPrefIcons(Preference preference, int color) {
  1121. if (preference != null) {
  1122. if (preference instanceof PreferenceGroup) {
  1123. PreferenceGroup group = ((PreferenceGroup) preference);
  1124. for (int i = 0; i < group.getPreferenceCount(); i++) {
  1125. tintPrefIcons(group.getPreference(i), color);
  1126. }
  1127. } else {
  1128. Drawable icon = preference.getIcon();
  1129. if (icon != null) {
  1130. icon.setColorFilter(color, PorterDuff.Mode.SRC_IN);
  1131. }
  1132. }
  1133. }
  1134. }
  1135. public static void tintPreferencesIcons(Context context, Preference preference) {
  1136. tintPrefIcons(preference, getColorFromAttribute(context, R.attr.textColorSecondary));
  1137. }
  1138. public static int getPreferredThumbnailWidth(Context context, boolean reset) {
  1139. if (preferredThumbnailWidth == -1 || reset) {
  1140. if (context != null) {
  1141. int width = context.getResources().getDisplayMetrics().widthPixels;
  1142. int height = context.getResources().getDisplayMetrics().heightPixels;
  1143. if (ConfigUtils.isTabletLayout()) {
  1144. width -= context.getResources().getDimensionPixelSize(R.dimen.message_fragment_width);
  1145. }
  1146. // width of thumbnail should be 60% of smallest display width
  1147. preferredThumbnailWidth = (int) ((float) width < height ? width * 0.6f : height * 0.6f);
  1148. }
  1149. }
  1150. return preferredThumbnailWidth;
  1151. }
  1152. public static int getPreferredAudioMessageWidth(Context context, boolean reset) {
  1153. if (preferredAudioMessageWidth == -1 || reset) {
  1154. if (context != null) {
  1155. int width = context.getResources().getDisplayMetrics().widthPixels;
  1156. int height = context.getResources().getDisplayMetrics().heightPixels;
  1157. if (ConfigUtils.isTabletLayout()) {
  1158. width -= context.getResources().getDimensionPixelSize(R.dimen.message_fragment_width);
  1159. }
  1160. // width of audio message should be 80% of smallest display width
  1161. preferredAudioMessageWidth = (int) ((float) width < height ? width * 0.75f : height * 0.75f);
  1162. }
  1163. }
  1164. return preferredAudioMessageWidth;
  1165. }
  1166. public static int getPreferredImageDimensions(@PreferenceService.ImageScale int imageScale) {
  1167. int maxSize = 0;
  1168. switch (imageScale) {
  1169. case PreferenceService.ImageScale_SMALL:
  1170. maxSize = 640;
  1171. break;
  1172. case PreferenceService.ImageScale_MEDIUM:
  1173. maxSize = 1024;
  1174. break;
  1175. case PreferenceService.ImageScale_LARGE:
  1176. maxSize = 1600;
  1177. break;
  1178. case PreferenceService.ImageScale_XLARGE:
  1179. maxSize = 2592;
  1180. break;
  1181. case PreferenceService.ImageScale_ORIGINAL:
  1182. maxSize = 65535;
  1183. break;
  1184. }
  1185. return maxSize;
  1186. }
  1187. public static int getCurrentScreenOrientation(Activity activity) {
  1188. int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
  1189. DisplayMetrics dm = new DisplayMetrics();
  1190. activity.getWindowManager().getDefaultDisplay().getMetrics(dm);
  1191. int width = dm.widthPixels;
  1192. int height = dm.heightPixels;
  1193. int orientation;
  1194. // if the device's natural orientation is portrait:
  1195. if ((rotation == Surface.ROTATION_0
  1196. || rotation == Surface.ROTATION_180) && height > width ||
  1197. (rotation == Surface.ROTATION_90
  1198. || rotation == Surface.ROTATION_270) && width > height) {
  1199. switch(rotation) {
  1200. case Surface.ROTATION_0:
  1201. orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
  1202. break;
  1203. case Surface.ROTATION_90:
  1204. orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
  1205. break;
  1206. case Surface.ROTATION_180:
  1207. orientation =
  1208. ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
  1209. break;
  1210. case Surface.ROTATION_270:
  1211. orientation =
  1212. ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
  1213. break;
  1214. default:
  1215. orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
  1216. break;
  1217. }
  1218. }
  1219. // if the device's natural orientation is landscape or if the device
  1220. // is square:
  1221. else {
  1222. switch(rotation) {
  1223. case Surface.ROTATION_0:
  1224. orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
  1225. break;
  1226. case Surface.ROTATION_90:
  1227. orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
  1228. break;
  1229. case Surface.ROTATION_180:
  1230. orientation =
  1231. ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
  1232. break;
  1233. case Surface.ROTATION_270:
  1234. orientation =
  1235. ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
  1236. break;
  1237. default:
  1238. orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
  1239. break;
  1240. }
  1241. }
  1242. return orientation;
  1243. }
  1244. /**
  1245. * Set app theme according to device theme if theme setting is set to "system"
  1246. * @param context
  1247. * @return
  1248. */
  1249. public static boolean refreshDeviceTheme(Context context) {
  1250. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
  1251. if (!BackupService.isRunning() && !RestoreService.isRunning()) {
  1252. SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getAppContext());
  1253. int themeIndex = Integer.parseInt(prefs.getString(context.getResources().getString(R.string.preferences__theme), String.valueOf(THEME_LIGHT)));
  1254. if (themeIndex == THEME_SYSTEM) {
  1255. int newTheme = (context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES ? THEME_DARK : THEME_LIGHT;
  1256. int oldTheme = ConfigUtils.getAppTheme(context);
  1257. if (oldTheme != newTheme) {
  1258. ConfigUtils.setAppTheme(newTheme);
  1259. return true;
  1260. }
  1261. }
  1262. }
  1263. }
  1264. return false;
  1265. }
  1266. /**
  1267. * Request desired orientation ignoring IllegalStateException on API 26 with targetApi == 28.
  1268. * Workaround for Android bug https://issuetracker.google.com/issues/68454482
  1269. * @param activity activity to request orientation for
  1270. * @param requestedOrientation requested orientation
  1271. */
  1272. public static void setRequestedOrientation(@NonNull Activity activity, int requestedOrientation) {
  1273. try {
  1274. activity.setRequestedOrientation(requestedOrientation);
  1275. } catch (IllegalStateException ignore) {}
  1276. }
  1277. /**
  1278. * Check if a particular app with packageName is installed on the system
  1279. * @param packageName
  1280. * @return true if app is installed, false otherwise or an error occured
  1281. */
  1282. public static boolean isAppInstalled(String packageName) {
  1283. try {
  1284. ThreemaApplication.getAppContext().getPackageManager().getPackageInfo(packageName, 0);
  1285. return true;
  1286. } catch (Exception e) {
  1287. return false;
  1288. }
  1289. }
  1290. /**
  1291. * Configure menu to display icons and dividers. Call this in onCreateOptionsMenu()
  1292. * @param context Context - required for themeing, set to null if you want the icon color not to be touched
  1293. * @param menu Menu to configure
  1294. */
  1295. @SuppressLint("RestrictedApi")
  1296. public static void addIconsToOverflowMenu(@Nullable Context context, @NonNull Menu menu) {
  1297. MenuCompat.setGroupDividerEnabled(menu, true);
  1298. try {
  1299. // restricted API
  1300. if (menu instanceof MenuBuilder) {
  1301. MenuBuilder menuBuilder = (MenuBuilder) menu;
  1302. menuBuilder.setOptionalIconsVisible(true);
  1303. if (context != null) {
  1304. ConfigUtils.themeMenu(menu, ConfigUtils.getColorFromAttribute(context, R.attr.textColorSecondary));
  1305. }
  1306. }
  1307. } catch (Exception ignored) {}
  1308. }
  1309. /**
  1310. * Return whether or not to use Threema Push, based on the build flavor and the preferences.
  1311. */
  1312. public static boolean useThreemaPush(@NonNull PreferenceService preferenceService) {
  1313. return BuildFlavor.forceThreemaPush() || preferenceService.useThreemaPush();
  1314. }
  1315. /**
  1316. * Return whether or not to use Threema Push, based on the build flavor and the preferences.
  1317. */
  1318. public static boolean useThreemaPush(@NonNull SharedPreferences rawSharedPreferences, @NonNull Context context) {
  1319. return BuildFlavor.forceThreemaPush()
  1320. || rawSharedPreferences.getBoolean(context.getString(R.string.preferences__threema_push_switch), false);
  1321. }
  1322. /**
  1323. * Return whether to show border around qr code to indicate its purpose
  1324. * @return true if borders are to be shown, false otherwise
  1325. */
  1326. public static boolean showQRCodeTypeBorders() {
  1327. return false;
  1328. }
  1329. /**
  1330. * Apply operations to content provider in small batches preventing TransactionTooLargeException
  1331. * @param authority Authority
  1332. * @param contentProviderOperations Operations to apply in smaller batches
  1333. * @throws OperationApplicationException
  1334. * @throws RemoteException
  1335. */
  1336. public static void applyToContentResolverInBatches(@NonNull String authority, ArrayList<ContentProviderOperation> contentProviderOperations) throws OperationApplicationException, RemoteException {
  1337. ContentResolver contentResolver = ThreemaApplication.getAppContext().getContentResolver();
  1338. for (int i = 0; i < contentProviderOperations.size(); i += CONTENT_PROVIDER_BATCH_SIZE) {
  1339. List<ContentProviderOperation> contentProviderOperationsBatch = contentProviderOperations.subList(i, Math.min(contentProviderOperations.size(), i + CONTENT_PROVIDER_BATCH_SIZE));
  1340. contentResolver.applyBatch(authority, new ArrayList<>(contentProviderOperationsBatch));
  1341. }
  1342. }
  1343. /**
  1344. * Return whether forward security features should be enabled.
  1345. */
  1346. public static boolean isForwardSecurityEnabled() {
  1347. return BuildConfig.FORWARD_SECURITY;
  1348. }
  1349. public static boolean isGroupAckEnabled() {
  1350. return true;
  1351. }
  1352. public static void clearAppData(Context context) {
  1353. ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
  1354. if (manager != null) {
  1355. manager.clearApplicationUserData();
  1356. }
  1357. }
  1358. /**
  1359. * @param context context required to know which string resource it is.
  1360. * @param id of the object
  1361. * @param quantity quantity of given values
  1362. * @param formatArgs how the string should be formatted
  1363. * @return returns the QuantityString or missing translation.
  1364. */
  1365. public static @NonNull
  1366. String getSafeQuantityString(@NonNull Context context, int id, int quantity, @NonNull Object... formatArgs) {
  1367. String result = "missing translation";
  1368. try {
  1369. result = context.getResources().getQuantityString(id, quantity, formatArgs);
  1370. } catch (Exception e) {
  1371. logger.error("Quantity String not found.", e);
  1372. }
  1373. return result;
  1374. }
  1375. }