OSDN Git Service

Various display-related fixes
[android-x86/packages-apps-Taskbar.git] / app / src / main / java / com / farmerbb / taskbar / util / U.java
1 /* Copyright 2016 Braden Farmer
2  *
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *     http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15
16 package com.farmerbb.taskbar.util;
17
18 import android.Manifest;
19 import android.accessibilityservice.AccessibilityService;
20 import android.annotation.SuppressLint;
21 import android.annotation.TargetApi;
22 import android.app.Activity;
23 import android.app.ActivityManager;
24 import android.app.ActivityOptions;
25 import android.app.AlertDialog;
26 import android.app.AppOpsManager;
27 import android.app.Service;
28 import android.content.ActivityNotFoundException;
29 import android.content.BroadcastReceiver;
30 import android.content.ComponentName;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.IntentFilter;
34 import android.content.SharedPreferences;
35 import android.content.pm.ActivityInfo;
36 import android.content.pm.ApplicationInfo;
37 import android.content.pm.LauncherActivityInfo;
38 import android.content.pm.LauncherApps;
39 import android.content.pm.PackageInfo;
40 import android.content.pm.PackageManager;
41 import android.content.pm.ResolveInfo;
42 import android.content.pm.ShortcutInfo;
43 import android.content.pm.ShortcutManager;
44 import android.content.pm.Signature;
45 import android.content.res.Configuration;
46 import android.graphics.Bitmap;
47 import android.graphics.BitmapFactory;
48 import android.graphics.Canvas;
49 import android.graphics.Color;
50 import android.graphics.Rect;
51 import android.graphics.drawable.BitmapDrawable;
52 import android.graphics.drawable.Drawable;
53 import android.hardware.display.DisplayManager;
54 import android.net.Uri;
55 import android.os.Build;
56 import android.os.Bundle;
57 import android.os.Handler;
58 import android.os.Looper;
59 import android.os.Process;
60 import android.os.UserHandle;
61 import android.os.UserManager;
62 import android.provider.Settings;
63
64 import androidx.annotation.DimenRes;
65 import androidx.annotation.VisibleForTesting;
66 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
67 import androidx.appcompat.view.ContextThemeWrapper;
68 import android.util.DisplayMetrics;
69 import android.view.Display;
70 import android.view.View;
71 import android.view.WindowManager;
72 import android.widget.ImageView;
73 import android.widget.Toast;
74
75 import com.farmerbb.taskbar.BuildConfig;
76 import com.farmerbb.taskbar.R;
77 import com.farmerbb.taskbar.activity.ContextMenuActivity;
78 import com.farmerbb.taskbar.activity.DummyActivity;
79 import com.farmerbb.taskbar.activity.InvisibleActivityFreeform;
80 import com.farmerbb.taskbar.activity.MainActivity;
81 import com.farmerbb.taskbar.activity.TouchAbsorberActivity;
82 import com.farmerbb.taskbar.helper.GlobalHelper;
83 import com.farmerbb.taskbar.helper.FreeformHackHelper;
84 import com.farmerbb.taskbar.helper.LauncherHelper;
85 import com.farmerbb.taskbar.helper.MenuHelper;
86 import com.farmerbb.taskbar.helper.ToastHelper;
87 import com.farmerbb.taskbar.service.DashboardService;
88 import com.farmerbb.taskbar.service.NotificationService;
89 import com.farmerbb.taskbar.service.PowerMenuService;
90 import com.farmerbb.taskbar.service.StartMenuService;
91 import com.farmerbb.taskbar.service.TaskbarService;
92
93 import java.io.BufferedInputStream;
94 import java.io.BufferedOutputStream;
95 import java.io.File;
96 import java.io.FileOutputStream;
97 import java.io.IOException;
98 import java.lang.reflect.Constructor;
99 import java.lang.reflect.Field;
100 import java.lang.reflect.Method;
101 import java.util.ArrayList;
102 import java.util.Arrays;
103 import java.util.List;
104 import java.util.Map;
105
106 import static com.farmerbb.taskbar.util.Constants.*;
107
108 public class U {
109
110     private U() {}
111
112     private static final int MAXIMIZED = 0;
113     private static final int LEFT = -1;
114     private static final int RIGHT = 1;
115
116     public static final int HIDDEN = 0;
117     public static final int TOP_APPS = 1;
118
119     // From android.app.ActivityManager.StackId
120     private static final int FULLSCREEN_WORKSPACE_STACK_ID = 1;
121     private static final int FREEFORM_WORKSPACE_STACK_ID = 2;
122
123     // From android.app.WindowConfiguration
124     private static final int WINDOWING_MODE_FULLSCREEN = 1;
125     private static final int WINDOWING_MODE_FREEFORM = 5;
126
127     public static final int EXPORT = 123;
128     public static final int IMPORT = 456;
129
130     public static final int IMAGE_REQUEST_CODE = 1001;
131
132     public static SharedPreferences getSharedPreferences(Context context) {
133         return context.getSharedPreferences(BuildConfig.APPLICATION_ID + "_preferences", Context.MODE_PRIVATE);
134     }
135
136     public static void showPermissionDialog(Context context) {
137         Callbacks callbacks = new Callbacks();
138         callbacks.onError = () -> showErrorDialog(context, "SYSTEM_ALERT_WINDOW", callbacks);
139
140         showPermissionDialog(context, callbacks);
141     }
142
143     public static AlertDialog showPermissionDialog(Context context, Callbacks callbacks) {
144         AlertDialog.Builder builder;
145         if(hasAndroidTVSettings(context))
146             builder = buildPermissionDialogAndroidTV(context, callbacks);
147         else
148             builder = buildPermissionDialogStandard(context, callbacks);
149
150         AlertDialog dialog = builder.create();
151         dialog.show();
152         dialog.setCancelable(false);
153
154         return dialog;
155     }
156
157     @TargetApi(Build.VERSION_CODES.M)
158     private static AlertDialog.Builder buildPermissionDialogStandard(Context context, Callbacks callbacks) {
159         String message = context.getString(R.string.tb_permission_dialog_message, getAppName(context))
160                 + context.getString(R.string.tb_permission_dialog_instructions_phone);
161
162         return new AlertDialog.Builder(context)
163                 .setTitle(R.string.tb_permission_dialog_title)
164                 .setMessage(message)
165                 .setPositiveButton(R.string.tb_action_grant_permission, (dialog, which) -> {
166                     try {
167                         Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.getPackageName()));
168                         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
169                         context.startActivity(intent);
170
171                         callbacks.onFinish.run();
172                     } catch (ActivityNotFoundException e) {
173                         callbacks.onError.run();
174                     }
175                 });
176     }
177
178     private static AlertDialog.Builder buildPermissionDialogAndroidTV(Context context, Callbacks callbacks) {
179         String message = context.getString(R.string.tb_permission_dialog_message, getAppName(context))
180                 + context.getString(R.string.tb_permission_dialog_instructions_tv);
181
182         return new AlertDialog.Builder(context)
183                 .setTitle(R.string.tb_permission_dialog_title)
184                 .setMessage(message)
185                 .setPositiveButton(R.string.tb_action_open_settings, (dialog, which) -> {
186                     try {
187                         Intent intent = new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS);
188                         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
189                         context.startActivity(intent);
190
191                         callbacks.onFinish.run();
192                     } catch (ActivityNotFoundException e) {
193                         callbacks.onError.run();
194                     }
195                 });
196     }
197
198     public static AlertDialog showErrorDialog(Context context, String appopCmd) {
199         return showErrorDialog(context, appopCmd, new Callbacks());
200     }
201
202     private static AlertDialog showErrorDialog(Context context, String appopCmd, Callbacks callbacks) {
203         AlertDialog.Builder builder = new AlertDialog.Builder(context);
204         builder.setTitle(R.string.tb_error_dialog_title)
205                 .setMessage(context.getString(R.string.tb_error_dialog_message, context.getPackageName(), appopCmd))
206                 .setPositiveButton(R.string.tb_action_ok, (dialog, which) -> callbacks.onFinish.run());
207
208         AlertDialog dialog = builder.create();
209         dialog.show();
210         dialog.setCancelable(false);
211
212         return dialog;
213     }
214
215     @TargetApi(Build.VERSION_CODES.P)
216     public static void lockDevice(Context context) {
217         sendAccessibilityAction(context, AccessibilityService.GLOBAL_ACTION_LOCK_SCREEN);
218     }
219
220     public static void sendAccessibilityAction(Context context, int action) {
221         sendAccessibilityAction(context, action, null);
222     }
223
224     public static void sendAccessibilityAction(Context context, int action, Runnable onComplete) {
225         setComponentEnabled(context, PowerMenuService.class, true);
226
227         boolean isAccessibilityServiceEnabled = isAccessibilityServiceEnabled(context);
228
229         if(!isAccessibilityServiceEnabled
230                 && hasWriteSecureSettingsPermission(context)) {
231             String services = Settings.Secure.getString(context.getContentResolver(),
232                     Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
233
234             String finalServices = services == null ? "" : services;
235
236             String powerMenuService = new ComponentName(context, PowerMenuService.class).flattenToString();
237
238             if(!finalServices.contains(powerMenuService)) {
239                 try {
240                     Settings.Secure.putString(context.getContentResolver(),
241                             Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
242                             finalServices.isEmpty()
243                                     ? powerMenuService
244                                     : finalServices + ":" + powerMenuService);
245                 } catch (Exception ignored) {}
246             }
247
248             newHandler().postDelayed(() -> {
249                 Intent intent = new Intent(ACTION_ACCESSIBILITY_ACTION);
250                 intent.putExtra(EXTRA_ACTION, action);
251                 sendBroadcast(context, intent);
252
253                 try {
254                     Settings.Secure.putString(context.getContentResolver(),
255                             Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
256                             finalServices);
257                 } catch (Exception ignored) {}
258
259                 if(onComplete != null) onComplete.run();
260             }, 100);
261         } else if(isAccessibilityServiceEnabled) {
262             Intent intent = new Intent(ACTION_ACCESSIBILITY_ACTION);
263             intent.putExtra(EXTRA_ACTION, action);
264             sendBroadcast(context, intent);
265
266             if(onComplete != null) onComplete.run();
267         } else {
268             launchApp(context, () -> {
269                 Intent intent = new Intent(context, DummyActivity.class);
270                 intent.putExtra("accessibility", true);
271                 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION);
272
273                 try {
274                     context.startActivity(intent, getActivityOptionsBundle(context, ApplicationType.APP_PORTRAIT, null));
275                 } catch (IllegalArgumentException | SecurityException ignored) {}
276             });
277         }
278     }
279
280     public static boolean isAccessibilityServiceEnabled(Context context) {
281         String accessibilityServices = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
282         ComponentName component = new ComponentName(context, PowerMenuService.class);
283
284         return accessibilityServices != null
285                 && (accessibilityServices.contains(component.flattenToString())
286                 || accessibilityServices.contains(component.flattenToShortString()));
287     }
288
289     public static boolean hasWriteSecureSettingsPermission(Context context) {
290         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
291                 && context.checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) == PackageManager.PERMISSION_GRANTED;
292     }
293
294     public static void showToast(Context context, int message) {
295         showToast(context, context.getString(message), Toast.LENGTH_SHORT);
296     }
297
298     public static void showToastLong(Context context, int message) {
299         showToast(context, context.getString(message), Toast.LENGTH_LONG);
300     }
301
302     public static void showToast(Context context, String message, int length) {
303         cancelToast();
304
305         ToastInterface toast = DependencyUtils.createToast(getDisplayContext(context), message, length);
306         toast.show();
307
308         ToastHelper.getInstance().setLastToast(toast);
309     }
310
311     public static void cancelToast() {
312         ToastInterface toast = ToastHelper.getInstance().getLastToast();
313         if(toast != null) toast.cancel();
314     }
315
316     public static void startShortcut(Context context, AppEntry entry, ShortcutInfo shortcut, View view) {
317         launchApp(context,
318                 entry,
319                 null,
320                 false,
321                 false,
322                 false,
323                 shortcut,
324                 view,
325                 null);
326     }
327
328     public static void launchApp(final Context context,
329                                  final AppEntry entry,
330                                  final String windowSize,
331                                  final boolean launchedFromTaskbar,
332                                  final boolean openInNewWindow,
333                                  final View view) {
334         launchApp(context,
335                 entry,
336                 windowSize,
337                 launchedFromTaskbar,
338                 false,
339                 openInNewWindow,
340                 null,
341                 view,
342                 null);
343     }
344
345     // Used for launching Persistent Shortcuts via the home screen or quick settings
346     public static void launchApp(final Context context,
347                                  final AppEntry entry,
348                                  final String windowSize,
349                                  final Runnable onError) {
350         launchApp(context,
351                 entry,
352                 windowSize,
353                 false,
354                 true,
355                 true,
356                 null,
357                 null,
358                 onError);
359     }
360
361     private static void launchApp(final Context context,
362                                   final AppEntry entry,
363                                   final String windowSize,
364                                   final boolean launchedFromTaskbar,
365                                   final boolean isPersistentShortcut,
366                                   final boolean openInNewWindow,
367                                   final ShortcutInfo shortcut,
368                                   final View view,
369                                   final Runnable onError) {
370         launchApp(context, launchedFromTaskbar, isPersistentShortcut, () ->
371                 continueLaunchingApp(context, entry, windowSize, openInNewWindow, shortcut, view, onError)
372         );
373     }
374
375     public static void launchApp(Context context, Runnable runnable) {
376         launchApp(context, true, false, runnable);
377     }
378
379     private static void launchApp(Context context, boolean launchedFromTaskbar, boolean isPersistentShortcut, Runnable runnable) {
380         SharedPreferences pref = getSharedPreferences(context);
381         FreeformHackHelper helper = FreeformHackHelper.getInstance();
382
383         boolean specialLaunch = hasBrokenSetLaunchBoundsApi()
384                 && helper.isInFreeformWorkspace()
385                 && MenuHelper.getInstance().isContextMenuOpen();
386
387         boolean noAnimation = pref.getBoolean(PREF_DISABLE_ANIMATIONS, false);
388         boolean isAndroidR = getCurrentApiVersion() > 29.0;
389
390         if(hasFreeformSupport(context)
391                 && (isFreeformModeEnabled(context) || isPersistentShortcut)
392                 && (!helper.isInFreeformWorkspace() || specialLaunch)) {
393             newHandler().postDelayed(() -> {
394                 startFreeformHack(context, true);
395
396                 newHandler().postDelayed(runnable, helper.isFreeformHackActive() ? 0 : isAndroidR ? 300 : 100);
397             }, launchedFromTaskbar ? 0 : 100);
398         } else
399             newHandler().postDelayed(runnable, !launchedFromTaskbar && noAnimation ? 100 : isAndroidR ? 100 : 0);
400     }
401
402     public static void startFreeformHack(Context context) {
403         startFreeformHack(context, false);
404     }
405
406     @TargetApi(Build.VERSION_CODES.N)
407     public static void startFreeformHack(Context context, boolean checkMultiWindow) {
408         Intent freeformHackIntent = new Intent(context, InvisibleActivityFreeform.class);
409         freeformHackIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
410                 | Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT
411                 | Intent.FLAG_ACTIVITY_NO_ANIMATION);
412
413         if(checkMultiWindow)
414             freeformHackIntent.putExtra("check_multiwindow", true);
415
416         if(canDrawOverlays(context))
417             startActivityLowerRight(context, freeformHackIntent);
418     }
419
420     public static void stopFreeformHack(Context context) {
421         sendBroadcast(context, ACTION_FINISH_FREEFORM_ACTIVITY);
422
423         if(isOverridingFreeformHack(context, false)) {
424             FreeformHackHelper helper = FreeformHackHelper.getInstance();
425             helper.setFreeformHackActive(false);
426             helper.setInFreeformWorkspace(false);
427         }
428     }
429
430     @TargetApi(Build.VERSION_CODES.N)
431     private static void continueLaunchingApp(Context context,
432                                              AppEntry entry,
433                                              String windowSize,
434                                              boolean openInNewWindow,
435                                              ShortcutInfo shortcut,
436                                              View view,
437                                              Runnable onError) {
438         SharedPreferences pref = getSharedPreferences(context);
439         Intent intent = new Intent();
440         intent.setComponent(ComponentName.unflattenFromString(entry.getComponentName()));
441         intent.setAction(Intent.ACTION_MAIN);
442         intent.addCategory(Intent.CATEGORY_LAUNCHER);
443         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
444         intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
445
446         if(FreeformHackHelper.getInstance().isInFreeformWorkspace()
447                 && Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1)
448             intent.addFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME);
449
450         if(pref.getBoolean(PREF_DISABLE_ANIMATIONS, false))
451             intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
452
453         boolean realOpenInNewWindow = openInNewWindow || pref.getBoolean(PREF_FORCE_NEW_WINDOW, false);
454         if(realOpenInNewWindow) applyOpenInNewWindow(context, intent);
455
456         ApplicationType type = getApplicationType(context, entry);
457
458         if(windowSize == null)
459             windowSize = SavedWindowSizes.getInstance(context).getWindowSize(context, entry.getPackageName());
460
461         Bundle bundle = getActivityOptionsBundle(context, type, windowSize, view);
462
463         prepareToStartActivity(context, realOpenInNewWindow, () -> {
464             if(shortcut == null) {
465                 UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
466                 if(entry.getUserId(context) == userManager.getSerialNumberForUser(Process.myUserHandle())) {
467                     try {
468                         context.startActivity(intent, bundle);
469                     } catch (ActivityNotFoundException e) {
470                         launchAndroidForWork(context, intent.getComponent(), bundle, entry.getUserId(context), onError);
471                     } catch (IllegalArgumentException | SecurityException ignored) {}
472                 } else
473                     launchAndroidForWork(context, intent.getComponent(), bundle, entry.getUserId(context), onError);
474             } else
475                 launchShortcut(context, shortcut, bundle, onError);
476         });
477
478         if(shouldCollapse(context, true)) {
479             sendBroadcast(context, ACTION_HIDE_TASKBAR);
480         } else {
481             sendBroadcast(context, ACTION_HIDE_START_MENU);
482         }
483     }
484
485     private static Bundle launchMode1(Context context, ApplicationType type, View view, int factor) {
486         DisplayInfo display = getDisplayInfo(context);
487
488         int width1 = display.width / factor;
489         int width2 = display.width - width1;
490         int height1 = display.height / factor;
491         int height2 = display.height - height1;
492
493         return getActivityOptionsBundle(context, type, view,
494                 width1,
495                 height1,
496                 width2,
497                 height2
498         );
499     }
500
501     private static Bundle launchMode2(Context context,
502                                       int launchType,
503                                       ApplicationType type,
504                                       View view) {
505         DisplayInfo display = getDisplayInfo(context);
506
507         int statusBarHeight = getStatusBarHeight(context);
508         String position = TaskbarPosition.getTaskbarPosition(context);
509
510         int orientation = getDisplayOrientation(context);
511         boolean isPortrait = orientation == Configuration.ORIENTATION_PORTRAIT;
512         boolean isLandscape = orientation == Configuration.ORIENTATION_LANDSCAPE;
513
514         int left = 0;
515         int top = statusBarHeight;
516         int right = display.width;
517         int bottom = display.height;
518
519         int iconSize = isOverridingFreeformHack(context) && !LauncherHelper.getInstance().isOnHomeScreen(context)
520                 ? 0 : context.getResources().getDimensionPixelSize(R.dimen.tb_icon_size);
521
522         if(TaskbarPosition.isVerticalLeft(position))
523             left = left + iconSize;
524         else if(TaskbarPosition.isVerticalRight(position))
525             right = right - iconSize;
526         else if(TaskbarPosition.isBottom(position))
527             bottom = bottom - iconSize;
528         else
529             top = top + iconSize;
530
531         int halfLandscape =
532                 (right / 2)
533                         + ((iconSize / 2) * (TaskbarPosition.isVerticalLeft(position) ? 1 : 0));
534         boolean isTopLeft = POSITION_TOP_LEFT.equals(position);
535         boolean isTopRight = POSITION_TOP_RIGHT.equals(position);
536         int halfPortrait =
537                 (bottom / 2) + ((iconSize / 2) * ((isTopLeft || isTopRight) ? 1 : 0));
538
539         if(launchType == RIGHT && isLandscape)
540             left = halfLandscape;
541         else if(launchType == RIGHT && isPortrait)
542             top = halfPortrait;
543         else if(launchType == LEFT && isLandscape)
544             right = halfLandscape;
545         else if(launchType == LEFT && isPortrait)
546             bottom = halfPortrait;
547
548         return getActivityOptionsBundle(context, type, view, left, top, right, bottom);
549     }
550
551     private static Bundle launchMode3(Context context, ApplicationType type, View view) {
552         DisplayInfo display = getDisplayInfo(context);
553
554         boolean isLandscape = type == ApplicationType.APP_LANDSCAPE;
555         int widthDimen = isLandscape ? R.dimen.tb_phone_size_height : R.dimen.tb_phone_size_width;
556         int heightDimen = isLandscape ? R.dimen.tb_phone_size_width : R.dimen.tb_phone_size_height;
557
558         int width1 = display.width / 2;
559         int width2 = context.getResources().getDimensionPixelSize(widthDimen) / 2;
560         int height1 = display.height / 2;
561         int height2 = context.getResources().getDimensionPixelSize(heightDimen) / 2;
562
563         return getActivityOptionsBundle(context, type, view,
564                 width1 - width2,
565                 height1 - height2,
566                 width1 + width2,
567                 height1 + height2
568         );
569     }
570
571     private static void launchAndroidForWork(Context context, ComponentName componentName, Bundle bundle, long userId, Runnable onError) {
572         UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
573         LauncherApps launcherApps = (LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE);
574
575         try {
576             launcherApps.startMainActivity(componentName, userManager.getUserForSerialNumber(userId), null, bundle);
577         } catch (ActivityNotFoundException | NullPointerException
578                 | IllegalStateException | SecurityException e) {
579             if(onError != null) launchApp(context, onError);
580         }
581     }
582
583     @TargetApi(Build.VERSION_CODES.N_MR1)
584     private static void launchShortcut(Context context, ShortcutInfo shortcut, Bundle bundle, Runnable onError) {
585         LauncherApps launcherApps = (LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE);
586
587         if(launcherApps.hasShortcutHostPermission()) {
588             try {
589                 launcherApps.startShortcut(shortcut, null, bundle);
590             } catch (ActivityNotFoundException | NullPointerException
591                     | IllegalStateException | SecurityException e) {
592                 if(onError != null) launchApp(context, onError);
593             }
594         }
595     }
596
597     private static void prepareToStartActivity(Context context, boolean openInNewWindow, Runnable runnable) {
598         sendBroadcast(context, ACTION_HIDE_CONTEXT_MENU);
599
600         if(!FreeformHackHelper.getInstance().isTouchAbsorberActive()
601                 && shouldLaunchTouchAbsorber(context)) {
602             startTouchAbsorberActivity(context);
603             newHandler().postDelayed(runnable, 100);
604         } else if(openInNewWindow) {
605             Intent intent = new Intent(context, DummyActivity.class);
606             intent.putExtra("finish_on_pause", true);
607             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
608                     | Intent.FLAG_ACTIVITY_NO_ANIMATION);
609             startActivityLowerRight(context, intent);
610
611             newHandler().postDelayed(runnable, 100);
612         } else
613             runnable.run();
614     }
615
616     public static void startActivityMaximized(Context context, Intent intent) {
617         Bundle bundle = launchMode2(context, MAXIMIZED, ApplicationType.CONTEXT_MENU, null);
618         prepareToStartActivity(context, false, () -> context.startActivity(intent, bundle));
619     }
620
621     public static void startActivityLowerRight(Context context, Intent intent) {
622         DisplayInfo display = getDisplayInfo(context);
623         try {
624             context.startActivity(intent,
625                     getActivityOptionsBundle(context, ApplicationType.FREEFORM_HACK, null,
626                             display.width,
627                             display.height,
628                             display.width + 1,
629                             display.height + 1
630                     ));
631         } catch (IllegalArgumentException | SecurityException ignored) {}
632     }
633
634     public static void startTouchAbsorberActivity(Context context) {
635         String position = TaskbarPosition.getTaskbarPosition(context);
636         DisplayInfo display = getDisplayInfo(context);
637
638         int left = 0;
639         int top = 0;
640         int right = display.width;
641         int bottom = display.height;
642
643         int iconSize = context.getResources().getDimensionPixelSize(R.dimen.tb_icon_size);
644
645         if(TaskbarPosition.isVerticalLeft(position))
646             right = iconSize;
647         else if(TaskbarPosition.isVerticalRight(position))
648             left = right - iconSize;
649         else if(TaskbarPosition.isBottom(position))
650             top = bottom - iconSize;
651         else
652             bottom = iconSize;
653
654         Intent intent = new Intent(context, TouchAbsorberActivity.class);
655         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
656         intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
657
658         try {
659             context.startActivity(intent,
660                     getActivityOptionsBundle(context, ApplicationType.FREEFORM_HACK, null,
661                             left, top, right, bottom));
662         } catch (IllegalArgumentException | SecurityException ignored) {}
663     }
664
665     public static void startContextMenuActivity(Context context, Bundle args) {
666         Intent intent = getThemedIntent(context, ContextMenuActivity.class);
667         intent.putExtra("args", args);
668         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
669
670         if(hasFreeformSupport(context) && FreeformHackHelper.getInstance().isInFreeformWorkspace()) {
671             DisplayInfo display = getDisplayInfo(context);
672
673             if(hasBrokenSetLaunchBoundsApi()) {
674                 intent.putExtra(EXTRA_CONTEXT_MENU_FIX, true);
675             }
676
677             context.startActivity(intent,
678                     getActivityOptionsBundle(context, ApplicationType.CONTEXT_MENU, null,
679                             0, 0, display.width, display.height));
680         } else
681             context.startActivity(intent);
682     }
683
684     public static void checkForUpdates(Context context) {
685         String url;
686         if(isPlayStoreRelease(context)) {
687             if(context.getPackageName().equals(BuildConfig.BASE_APPLICATION_ID)
688                     && !isPlayStoreInstalled(context))
689                 url = "https://github.com/farmerbb/Taskbar/releases";
690             else
691                 url = "https://play.google.com/store/apps/details?id=" + context.getPackageName();
692         } else
693             url = "https://f-droid.org/packages/" + context.getPackageName();
694
695         Intent intent = new Intent(Intent.ACTION_VIEW);
696         intent.setData(Uri.parse(url));
697         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
698
699         try {
700             context.startActivity(intent);
701         } catch (ActivityNotFoundException ignored) {}
702     }
703
704     public static boolean launcherIsDefault(Context context) {
705         Intent homeIntent = new Intent(Intent.ACTION_MAIN);
706         homeIntent.addCategory(Intent.CATEGORY_HOME);
707         ResolveInfo defaultLauncher = context.getPackageManager().resolveActivity(homeIntent, PackageManager.MATCH_DEFAULT_ONLY);
708
709         try {
710             return defaultLauncher.activityInfo.packageName.equals(context.getPackageName());
711         } catch (NullPointerException e) {
712             return false;
713         }
714     }
715
716     private static int getMaxNumOfColumns(Context context) {
717         SharedPreferences pref = getSharedPreferences(context);
718         DisplayInfo display = getDisplayInfo(context);
719         float density = display.currentDensity / 160.0f;
720         float baseTaskbarSize = getBaseTaskbarSize(context) / density;
721         int numOfColumns = 0;
722
723         float maxScreenSize = TaskbarPosition.isVertical(context)
724                 ? (display.height - getStatusBarHeight(context)) / density
725                 : display.width / density;
726
727         float iconSize = context.getResources().getDimension(R.dimen.tb_icon_size) / density;
728
729         int userMaxNumOfColumns = Integer.parseInt(pref.getString(PREF_MAX_NUM_OF_RECENTS, "10"));
730
731         while(baseTaskbarSize + iconSize < maxScreenSize
732                 && numOfColumns < userMaxNumOfColumns) {
733             baseTaskbarSize = baseTaskbarSize + iconSize;
734             numOfColumns++;
735         }
736
737         return numOfColumns;
738     }
739
740     public static int getMaxNumOfEntries(Context context) {
741         SharedPreferences pref = getSharedPreferences(context);
742         return pref.getBoolean(PREF_DISABLE_SCROLLING_LIST, false)
743                 ? getMaxNumOfColumns(context)
744                 : Integer.parseInt(pref.getString(PREF_MAX_NUM_OF_RECENTS, "10"));
745     }
746
747     public static int getStatusBarHeight(Context context) {
748         return LauncherHelper.getInstance().isOnSecondaryHomeScreen(context)
749                 ? 0 : getSystemDimen(context, "status_bar_height");
750     }
751
752     public static int getNavbarHeight(Context context) {
753         SharedPreferences pref = getSharedPreferences(context);
754         boolean isNavbarHidden = isShowHideNavbarSupported()
755                 && LauncherHelper.getInstance().isOnSecondaryHomeScreen(context)
756                 && pref.getBoolean(PREF_AUTO_HIDE_NAVBAR_DESKTOP_MODE, false);
757
758         return isNavbarHidden ? 0 : getSystemDimen(context, "navigation_bar_height");
759     }
760
761     private static int getSystemDimen(Context context, String id) {
762         context = getDisplayContext(context);
763
764         int value = 0;
765         int resourceId = context.getResources().getIdentifier(id, "dimen", "android");
766         if(resourceId > 0)
767             value = context.getResources().getDimensionPixelSize(resourceId);
768
769         return value;
770     }
771
772     public static void refreshPinnedIcons(Context context) {
773         IconCache.getInstance(context).clearCache();
774
775         PinnedBlockedApps pba = PinnedBlockedApps.getInstance(context);
776         List<AppEntry> pinnedAppsList = new ArrayList<>(pba.getPinnedApps());
777         List<AppEntry> blockedAppsList = new ArrayList<>(pba.getBlockedApps());
778         PackageManager pm = context.getPackageManager();
779
780         pba.clear(context);
781
782         for(AppEntry entry : pinnedAppsList) {
783             UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
784             LauncherApps launcherApps = (LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE);
785
786             final List<UserHandle> userHandles = userManager.getUserProfiles();
787             LauncherActivityInfo appInfo = null;
788
789             for(UserHandle handle : userHandles) {
790                 List<LauncherActivityInfo> list = launcherApps.getActivityList(entry.getPackageName(), handle);
791                 if(!list.isEmpty()) {
792                     // Google App workaround
793                     if(!entry.getPackageName().equals("com.google.android.googlequicksearchbox"))
794                         appInfo = list.get(0);
795                     else {
796                         boolean added = false;
797                         for(LauncherActivityInfo info : list) {
798                             if(info.getName().equals("com.google.android.googlequicksearchbox.SearchActivity")) {
799                                 appInfo = info;
800                                 added = true;
801                             }
802                         }
803
804                         if(!added) appInfo = list.get(0);
805                     }
806
807                     break;
808                 }
809             }
810
811             if(appInfo != null) {
812                 AppEntry newEntry = new AppEntry(
813                         entry.getPackageName(),
814                         entry.getComponentName(),
815                         entry.getLabel(),
816                         IconCache.getInstance(context).getIcon(context, pm, appInfo),
817                         true);
818
819                 newEntry.setUserId(entry.getUserId(context));
820                 pba.addPinnedApp(context, newEntry);
821             }
822         }
823
824         for(AppEntry entry : blockedAppsList) {
825             pba.addBlockedApp(context, entry);
826         }
827     }
828
829     public static boolean canEnableFreeform() {
830         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
831     }
832
833     @TargetApi(Build.VERSION_CODES.N)
834     public static boolean hasFreeformSupport(Context context) {
835         return canEnableFreeform()
836                 && (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT)
837                 || Settings.Global.getInt(context.getContentResolver(), "enable_freeform_support", 0) != 0
838                 || (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1
839                 && Settings.Global.getInt(context.getContentResolver(), "force_resizable_activities", 0) != 0));
840     }
841
842     public static boolean canBootToFreeform(Context context) {
843         return canBootToFreeform(context, true);
844     }
845
846     private static boolean canBootToFreeform(Context context, boolean checkPref) {
847         return hasFreeformSupport(context) && !isOverridingFreeformHack(context, checkPref);
848     }
849
850     public static boolean isSamsungDevice() {
851         return Build.MANUFACTURER.equalsIgnoreCase("Samsung");
852     }
853
854     private static boolean isNvidiaDevice() {
855         return Build.MANUFACTURER.equalsIgnoreCase("NVIDIA");
856     }
857
858     public static boolean isServiceRunning(Context context, Class<? extends Service> cls) {
859         if(LauncherHelper.getInstance().isOnSecondaryHomeScreen(context)
860                 && (cls.equals(TaskbarService.class)
861                 || cls.equals(StartMenuService.class)
862                 || cls.equals(DashboardService.class)))
863             return true;
864
865         return isServiceRunning(context, cls.getName());
866     }
867
868     private static boolean isServiceRunning(Context context, String className) {
869         ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
870         for(ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
871             if(className.equals(service.service.getClassName()))
872                 return true;
873         }
874
875         return false;
876     }
877
878     public static int getBackgroundTint(Context context) {
879         SharedPreferences pref = getSharedPreferences(context);
880
881         // Import old background tint preference
882         if(pref.contains(PREF_SHOW_BACKGROUND)) {
883             SharedPreferences.Editor editor = pref.edit();
884
885             if(!pref.getBoolean(PREF_SHOW_BACKGROUND, true))
886                 editor.putInt(PREF_BACKGROUND_TINT, Color.TRANSPARENT).apply();
887
888             editor.remove(PREF_SHOW_BACKGROUND);
889             editor.apply();
890         }
891
892         return pref.getInt(PREF_BACKGROUND_TINT, context.getResources().getInteger(R.integer.tb_translucent_gray));
893     }
894
895     public static int getAccentColor(Context context) {
896         SharedPreferences pref = getSharedPreferences(context);
897         return pref.getInt(PREF_ACCENT_COLOR, context.getResources().getInteger(R.integer.tb_translucent_white));
898     }
899
900     public static boolean canDrawOverlays(Context context) {
901         return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays(context);
902     }
903
904     public static boolean isGame(Context context, String packageName) {
905         SharedPreferences pref = getSharedPreferences(context);
906         if(pref.getBoolean(PREF_LAUNCH_GAMES_FULLSCREEN, true)) {
907             PackageManager pm = context.getPackageManager();
908
909             try {
910                 ApplicationInfo info = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
911                 return (info.flags & ApplicationInfo.FLAG_IS_GAME) != 0 || (info.metaData != null && info.metaData.getBoolean("isGame", false));
912             } catch (PackageManager.NameNotFoundException e) {
913                 return false;
914             }
915         } else
916             return false;
917     }
918
919     private static ActivityOptions getActivityOptions(View view) {
920         return getActivityOptions(null, null, view);
921     }
922
923     public static ActivityOptions getActivityOptions(Context context, ApplicationType applicationType, View view) {
924         ActivityOptions options;
925         if(view != null) {
926             if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
927                 options = ActivityOptions.makeClipRevealAnimation(view, 0, 0, view.getWidth(), view.getHeight());
928             else
929                 options = ActivityOptions.makeScaleUpAnimation(view, 0, 0, view.getWidth(), view.getHeight());
930         } else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
931             options = ActivityOptions.makeBasic();
932         else {
933             try {
934                 Constructor<ActivityOptions> constructor = ActivityOptions.class.getDeclaredConstructor();
935                 constructor.setAccessible(true);
936                 options = constructor.newInstance();
937             } catch (Exception e) {
938                 return null;
939             }
940         }
941
942         if(applicationType == null)
943             return options;
944
945         int stackId = -1;
946
947         switch(applicationType) {
948             case APP_PORTRAIT:
949             case APP_LANDSCAPE:
950                 if(FreeformHackHelper.getInstance().isFreeformHackActive())
951                     stackId = getFreeformWindowModeId();
952                 else
953                     stackId = getFullscreenWindowModeId();
954                 break;
955             case APP_FULLSCREEN:
956                 stackId = getFullscreenWindowModeId();
957                 break;
958             case FREEFORM_HACK:
959                 stackId = getFreeformWindowModeId();
960                 break;
961             case CONTEXT_MENU:
962                 if(hasBrokenSetLaunchBoundsApi()
963                         || (!isChromeOs(context) && getCurrentApiVersion() >= 28.0f)
964                         || (isChromeOs(context) && getCurrentApiVersion() >= 30.0f))
965                     stackId = getFullscreenWindowModeId();
966                 break;
967         }
968
969         if(stackId != -1) {
970             allowReflection();
971             try {
972                 Method method = ActivityOptions.class.getMethod(getWindowingModeMethodName(), int.class);
973                 method.invoke(options, stackId);
974             } catch (Exception ignored) {}
975         }
976
977         if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
978             int launchDisplayId = LauncherHelper.getInstance().getSecondaryDisplayId();
979             if(launchDisplayId != -1)
980                 options.setLaunchDisplayId(launchDisplayId);
981         }
982
983         return options;
984     }
985
986     private static int getFullscreenWindowModeId() {
987         if(getCurrentApiVersion() >= 28.0f)
988             return WINDOWING_MODE_FULLSCREEN;
989         else
990             return FULLSCREEN_WORKSPACE_STACK_ID;
991     }
992
993     private static int getFreeformWindowModeId() {
994         if(getCurrentApiVersion() >= 28.0f)
995             return WINDOWING_MODE_FREEFORM;
996         else
997             return FREEFORM_WORKSPACE_STACK_ID;
998     }
999
1000     private static String getWindowingModeMethodName() {
1001         if(getCurrentApiVersion() >= 28.0f)
1002             return "setLaunchWindowingMode";
1003         else
1004             return "setLaunchStackId";
1005     }
1006
1007     public static Bundle getActivityOptionsBundle(Context context, ApplicationType type, View view) {
1008         SharedPreferences pref = getSharedPreferences(context);
1009         return getActivityOptionsBundle(context, type, pref.getString(PREF_WINDOW_SIZE, "standard"), view);
1010     }
1011
1012     private static Bundle getActivityOptionsBundle(Context context, ApplicationType type, String windowSize, View view) {
1013         if(!canEnableFreeform() || !isFreeformModeEnabled(context))
1014             return getActivityOptions(view).toBundle();
1015
1016         switch(windowSize) {
1017             case "standard":
1018                 if(getCurrentApiVersion() > 29.0f)
1019                     return launchMode1(context, type, view, 4);
1020                 break;
1021             case "large":
1022                 return launchMode1(context, type, view, 8);
1023             case "fullscreen":
1024                 return launchMode2(context, MAXIMIZED, type, view);
1025             case "half_left":
1026                 return launchMode2(context, LEFT, type, view);
1027             case "half_right":
1028                 return launchMode2(context, RIGHT, type, view);
1029             case "phone_size":
1030                 return launchMode3(context, type, view);
1031         }
1032
1033         return getActivityOptions(context, type, view).toBundle();
1034     }
1035
1036     private static Bundle getActivityOptionsBundle(Context context,
1037                                                    ApplicationType applicationType,
1038                                                    View view,
1039                                                    int left,
1040                                                    int top,
1041                                                    int right,
1042                                                    int bottom) {
1043         ActivityOptions options = getActivityOptions(context, applicationType, view);
1044         if(options == null) return null;
1045
1046         if(Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
1047             return options.toBundle();
1048
1049         return options.setLaunchBounds(new Rect(left, top, right, bottom)).toBundle();
1050     }
1051
1052     @SuppressLint("SwitchIntDef")
1053     private static ApplicationType getApplicationType(Context context, AppEntry entry) {
1054         if(isGame(context, entry.getPackageName()))
1055             return ApplicationType.APP_FULLSCREEN;
1056
1057         try {
1058             ActivityInfo info = context.getPackageManager().getActivityInfo(
1059                     ComponentName.unflattenFromString(entry.getComponentName()),
1060                     0
1061             );
1062
1063             switch(info.screenOrientation) {
1064                 case ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE:
1065                 case ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE:
1066                 case ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE:
1067                 case ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE:
1068                     return ApplicationType.APP_LANDSCAPE;
1069
1070                 case ActivityInfo.SCREEN_ORIENTATION_PORTRAIT:
1071                 case ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT:
1072                 case ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT:
1073                 case ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT:
1074                     return ApplicationType.APP_PORTRAIT;
1075             }
1076         } catch (PackageManager.NameNotFoundException ignored) {}
1077
1078         return context.getPackageName().equals(BuildConfig.ANDROIDX86_APPLICATION_ID)
1079                 ? ApplicationType.APP_LANDSCAPE
1080                 : ApplicationType.APP_PORTRAIT;
1081     }
1082
1083     public static boolean isSystemApp(Context context) {
1084         try {
1085             ApplicationInfo info = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
1086             int mask = ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
1087             return (info.flags & mask) != 0;
1088         } catch (PackageManager.NameNotFoundException e) {
1089             return false;
1090         }
1091     }
1092
1093     public static boolean isChromeOs(Context context) {
1094         return context.getPackageManager().hasSystemFeature("org.chromium.arc");
1095     }
1096
1097     @TargetApi(Build.VERSION_CODES.O)
1098     public static boolean isAndroidTV(Context context) {
1099         return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK_ONLY);
1100     }
1101
1102     public static boolean isBlissOs(Context context) {
1103         boolean validBlissOsBuildProp = false;
1104
1105         String blissVersion = getSystemProperty("ro.bliss.version");
1106         if(blissVersion != null && !blissVersion.isEmpty())
1107             validBlissOsBuildProp = true;
1108
1109         return validBlissOsBuildProp
1110                 && context.getPackageName().equals(BuildConfig.BASE_APPLICATION_ID)
1111                 && isSystemApp(context);
1112     }
1113
1114     public static boolean isAndroidGeneric(Context context) {
1115         if(isBlissOs(context)) return true;
1116
1117         boolean validAndroidGenericBuildProp = false;
1118
1119         String manufacturer = getSystemProperty("ro.ag.product.manufacturer");
1120         if(manufacturer != null && manufacturer.equals("Android-Generic"))
1121             validAndroidGenericBuildProp = true;
1122
1123         return validAndroidGenericBuildProp
1124                 && context.getPackageName().equals(BuildConfig.BASE_APPLICATION_ID)
1125                 && isSystemApp(context);
1126     }
1127
1128     public static boolean isLauncherPermanentlyEnabled(Context context) {
1129         if(context.getPackageName().equals(BuildConfig.ANDROIDX86_APPLICATION_ID))
1130             return true;
1131
1132         return hasSupportLibrary(context, 0);
1133     }
1134
1135     public static boolean hasSupportLibrary(Context context, int minVersion) {
1136         PackageManager pm = context.getPackageManager();
1137         try {
1138             PackageInfo pInfo = pm.getPackageInfo(BuildConfig.SUPPORT_APPLICATION_ID, 0);
1139             return pInfo.versionCode >= minVersion
1140                     && pm.checkSignatures(BuildConfig.SUPPORT_APPLICATION_ID, context.getPackageName()) == PackageManager.SIGNATURE_MATCH
1141                     && context.getPackageName().equals(BuildConfig.BASE_APPLICATION_ID)
1142                     && isSystemApp(context);
1143         } catch (PackageManager.NameNotFoundException e) {
1144             return false;
1145         }
1146     }
1147
1148     @VisibleForTesting
1149     public static float getBaseTaskbarSize(Context context) {
1150         return getBaseTaskbarSizeStart(context) + getBaseTaskbarSizeEnd(context, null);
1151     }
1152
1153     public static float getBaseTaskbarSizeStart(Context context) {
1154         SharedPreferences pref = getSharedPreferences(context);
1155         float baseTaskbarSize = context.getResources().getDimension(R.dimen.tb_base_size_start_plus_divider);
1156
1157         baseTaskbarSize += pref.getBoolean(PREF_ALT_BUTTON_CONFIG, false)
1158                 ? context.getResources().getDimension(R.dimen.tb_base_size_collapse_button) : 0;
1159
1160         boolean navbarButtonsEnabled = false;
1161
1162         if(getBooleanPrefWithDefault(context, PREF_DASHBOARD))
1163             baseTaskbarSize += context.getResources().getDimension(R.dimen.tb_dashboard_button_size);
1164
1165         if(pref.getBoolean(PREF_BUTTON_BACK, false)) {
1166             navbarButtonsEnabled = true;
1167             baseTaskbarSize += context.getResources().getDimension(R.dimen.tb_icon_size);
1168         }
1169
1170         if(pref.getBoolean(PREF_BUTTON_HOME, false)) {
1171             navbarButtonsEnabled = true;
1172             baseTaskbarSize += context.getResources().getDimension(R.dimen.tb_icon_size);
1173         }
1174
1175         if(pref.getBoolean(PREF_BUTTON_RECENTS, false)) {
1176             navbarButtonsEnabled = true;
1177             baseTaskbarSize += context.getResources().getDimension(R.dimen.tb_icon_size);
1178         }
1179
1180         if(navbarButtonsEnabled)
1181             baseTaskbarSize += context.getResources().getDimension(R.dimen.tb_navbar_buttons_margin);
1182
1183         return baseTaskbarSize;
1184     }
1185
1186     public static float getBaseTaskbarSizeEnd(Context context, Map<Integer, Boolean> sysTrayIconStates) {
1187         SharedPreferences pref = getSharedPreferences(context);
1188         float baseTaskbarSize = pref.getBoolean(PREF_ALT_BUTTON_CONFIG, false)
1189                 ? 0 : context.getResources().getDimension(R.dimen.tb_base_size_collapse_button);
1190
1191         if(isSystemTrayEnabled(context)) {
1192             float sysTraySize = context.getResources().getDimension(R.dimen.tb_systray_size);
1193
1194             if(sysTrayIconStates != null) {
1195                 for(Integer key : sysTrayIconStates.keySet()) {
1196                     if(!sysTrayIconStates.get(key)) {
1197                         sysTraySize -= context.getResources().getDimension(key == R.id.notification_count
1198                                 ? R.dimen.tb_systray_icon_size_notifications : R.dimen.tb_systray_icon_size);
1199                     }
1200                 }
1201             }
1202
1203             baseTaskbarSize += sysTraySize;
1204         }
1205
1206         return baseTaskbarSize;
1207     }
1208
1209     private static void startTaskbarService(Context context, boolean fullRestart) {
1210         context.startService(new Intent(context, TaskbarService.class));
1211         context.startService(new Intent(context, StartMenuService.class));
1212         context.startService(new Intent(context, DashboardService.class));
1213         if(fullRestart) context.startService(new Intent(context, NotificationService.class));
1214     }
1215
1216     private static void stopTaskbarService(Context context, boolean fullRestart) {
1217         context.stopService(new Intent(context, TaskbarService.class));
1218         context.stopService(new Intent(context, StartMenuService.class));
1219         context.stopService(new Intent(context, DashboardService.class));
1220         if(fullRestart) context.stopService(new Intent(context, NotificationService.class));
1221     }
1222
1223     public static void restartTaskbar(Context context) {
1224         SharedPreferences pref = getSharedPreferences(context);
1225         if(pref.getBoolean(PREF_TASKBAR_ACTIVE, false)
1226                 && !pref.getBoolean(PREF_IS_HIDDEN, false)) {
1227             pref.edit()
1228                     .putBoolean(PREF_IS_RESTARTING, true)
1229                     .putBoolean(PREF_SKIP_AUTO_HIDE_NAVBAR, true)
1230                     .apply();
1231
1232             stopTaskbarService(context, true);
1233             startTaskbarService(context, true);
1234         } else if(isServiceRunning(context, StartMenuService.class)) {
1235             pref.edit().putBoolean(PREF_SKIP_AUTO_HIDE_NAVBAR, true).apply();
1236
1237             stopTaskbarService(context, false);
1238             startTaskbarService(context, false);
1239         }
1240
1241         sendBroadcast(context, ACTION_RESTART);
1242     }
1243
1244     public static void restartNotificationService(Context context) {
1245         if(isServiceRunning(context, NotificationService.class)) {
1246             SharedPreferences pref = getSharedPreferences(context);
1247             pref.edit().putBoolean(PREF_IS_RESTARTING, true).apply();
1248
1249             Intent intent = new Intent(context, NotificationService.class);
1250             context.stopService(intent);
1251             context.startService(intent);
1252         }
1253     }
1254
1255     public static void showHideNavigationBar(Context context, boolean show) {
1256         showHideNavigationBar(context, getTaskbarDisplayID(context), show, 0);
1257     }
1258
1259     public static void showHideNavigationBar(Context context, int displayID, boolean show, int delay) {
1260         if(!isShowHideNavbarSupported()
1261                 || (!isDesktopModeActive(context)
1262                 && !isBlissOs(context)
1263                 && !hasSupportLibrary(context, 7))) {
1264             return;
1265         }
1266
1267         Lazy<Integer> value = () -> show ? 0 : getSystemDimen(context, "navigation_bar_height") * -1;
1268
1269         if(hasWriteSecureSettingsPermission(context)) {
1270             Runnable runnable = () -> {
1271                 try {
1272                     setOverscan(displayID, value.get());
1273                 } catch (Exception ignored) {}
1274             };
1275
1276             if(delay == 0)
1277                 runnable.run();
1278             else
1279                 newHandler().postDelayed(runnable, delay);
1280
1281             return;
1282         }
1283
1284         if(hasSupportLibrary(context, 7)) {
1285             Intent intent = new Intent(BuildConfig.SUPPORT_APPLICATION_ID + ".CHANGE_OVERSCAN");
1286             intent.setPackage(BuildConfig.SUPPORT_APPLICATION_ID);
1287
1288             intent.putExtra("display_id", displayID);
1289             intent.putExtra("value", value.get());
1290
1291             context.sendBroadcast(intent);
1292             return;
1293         }
1294
1295         // Show or hide the system navigation bar on Bliss-x86
1296         if(!isBlissOs(context)) return;
1297
1298         try {
1299             if(getCurrentApiVersion() >= 28.0f)
1300                 Settings.Secure.putInt(context.getContentResolver(), "navigation_bar_visible", show ? 1 : 0);
1301             else
1302                 Settings.System.putInt(context.getContentResolver(), "navigation_bar_show", show ? 1 : 0);
1303         } catch (Exception ignored) {}
1304     }
1305
1306     public static boolean isShowHideNavbarSupported() {
1307         return getCurrentApiVersion() <= 29.0f;
1308     }
1309
1310     public static void initPrefs(Context context) {
1311         // Enable freeform hack automatically on supported devices
1312         SharedPreferences pref = getSharedPreferences(context);
1313         if(canEnableFreeform()) {
1314             if(!pref.getBoolean(PREF_FREEFORM_HACK_OVERRIDE, false)) {
1315                 pref.edit()
1316                         .putBoolean(PREF_FREEFORM_HACK, hasFreeformSupport(context) && !isSamsungDevice())
1317                         .putBoolean(PREF_SAVE_WINDOW_SIZES, false)
1318                         .putBoolean(PREF_FREEFORM_HACK_OVERRIDE, true)
1319                         .apply();
1320             } else if(!hasFreeformSupport(context)) {
1321                 pref.edit().putBoolean(PREF_FREEFORM_HACK, false).apply();
1322
1323                 stopFreeformHack(context);
1324             }
1325         } else {
1326             boolean freeformWasEnabled = isFreeformModeEnabled(context)
1327                     || pref.getBoolean(PREF_SHOW_FREEFORM_DISABLED_MESSAGE, false);
1328
1329             pref.edit()
1330                     .putBoolean(PREF_FREEFORM_HACK, false)
1331                     .putBoolean(PREF_SHOW_FREEFORM_DISABLED_MESSAGE, freeformWasEnabled)
1332                     .apply();
1333
1334             SavedWindowSizes.getInstance(context).clear(context);
1335             stopFreeformHack(context);
1336         }
1337
1338         // Customizations for BlissOS
1339         if(isAndroidGeneric(context) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
1340                 && !pref.getBoolean(PREF_BLISS_OS_PREFS, false)) {
1341             SharedPreferences.Editor editor = pref.edit();
1342
1343             if(hasFreeformSupport(context)) {
1344                 editor.putBoolean(PREF_FREEFORM_HACK, true);
1345             }
1346
1347             editor.putString(PREF_START_BUTTON_IMAGE, PREF_START_BUTTON_IMAGE_APP_LOGO);
1348             editor.putString(PREF_RECENTS_AMOUNT, PREF_RECENTS_AMOUNT_RUNNING_APPS_ONLY);
1349             editor.putString(PREF_REFRESH_FREQUENCY, "0");
1350             editor.putString(PREF_MAX_NUM_OF_RECENTS, "2147483647");
1351             editor.putString(PREF_SORT_ORDER, "true");
1352             editor.putBoolean(PREF_BUTTON_BACK, true);
1353             editor.putBoolean(PREF_BUTTON_HOME, true);
1354             editor.putBoolean(PREF_BUTTON_RECENTS, true);
1355             editor.putBoolean(PREF_AUTO_HIDE_NAVBAR, true);
1356             editor.putBoolean(PREF_SHORTCUT_ICON, false);
1357             editor.putBoolean(PREF_BLISS_OS_PREFS, true);
1358             editor.apply();
1359         }
1360
1361         // Customizations for Android-x86 devices (non-Bliss)
1362         if(context.getPackageName().equals(BuildConfig.ANDROIDX86_APPLICATION_ID)
1363                 && isSystemApp(context)
1364                 && !pref.getBoolean(PREF_ANDROID_X86_PREFS, false)) {
1365             pref.edit()
1366                     .putString(PREF_RECENTS_AMOUNT, PREF_RECENTS_AMOUNT_RUNNING_APPS_ONLY)
1367                     .putString(PREF_REFRESH_FREQUENCY, "0")
1368                     .putString(PREF_MAX_NUM_OF_RECENTS, "2147483647")
1369                     .putString(PREF_SORT_ORDER, "true")
1370                     .putBoolean(PREF_SHORTCUT_ICON, false)
1371                     .putBoolean(PREF_ANDROID_X86_PREFS, true)
1372                     .apply();
1373         }
1374     }
1375
1376     public static DisplayInfo getDisplayInfo(Context context) {
1377         context = getDisplayContext(context);
1378         int displayID = getTaskbarDisplayID(context);
1379
1380         DisplayManager dm = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
1381         Display currentDisplay = null;
1382
1383         for(Display display : dm.getDisplays()) {
1384             if(display.getDisplayId() == displayID) {
1385                 currentDisplay = display;
1386                 break;
1387             }
1388         }
1389
1390         if(currentDisplay == null)
1391             return new DisplayInfo(0, 0, 0, 0, false);
1392
1393         DisplayMetrics metrics = new DisplayMetrics();
1394         currentDisplay.getRealMetrics(metrics);
1395
1396         boolean displayDefaultsToFreeform = displayDefaultsToFreeform(context, currentDisplay);
1397         return new DisplayInfo(metrics.widthPixels, metrics.heightPixels, metrics.densityDpi, 0, displayDefaultsToFreeform);
1398     }
1399
1400     private static int getTaskbarDisplayID(Context context) {
1401         LauncherHelper helper = LauncherHelper.getInstance();
1402
1403         if(helper.isOnSecondaryHomeScreen(context))
1404             return helper.getSecondaryDisplayId();
1405         else
1406             return Display.DEFAULT_DISPLAY;
1407     }
1408
1409     public static void pinAppShortcut(Context context) {
1410         if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1411             ShortcutManager mShortcutManager = context.getSystemService(ShortcutManager.class);
1412
1413             if(mShortcutManager.isRequestPinShortcutSupported()) {
1414                 ShortcutInfo pinShortcutInfo = new ShortcutInfo.Builder(context, "freeform_mode").build();
1415
1416                 mShortcutManager.requestPinShortcut(pinShortcutInfo, null);
1417             } else
1418                 showToastLong(context, R.string.tb_pin_shortcut_not_supported);
1419         } else {
1420             Intent intent = ShortcutUtils.getShortcutIntent(context);
1421             intent.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
1422             intent.putExtra("duplicate", false);
1423
1424             Intent homeIntent = new Intent(Intent.ACTION_MAIN);
1425             homeIntent.addCategory(Intent.CATEGORY_HOME);
1426             ResolveInfo defaultLauncher = context.getPackageManager().resolveActivity(homeIntent, PackageManager.MATCH_DEFAULT_ONLY);
1427
1428             intent.setPackage(defaultLauncher.activityInfo.packageName);
1429             context.sendBroadcast(intent);
1430
1431             showToast(context, R.string.tb_shortcut_created);
1432         }
1433     }
1434
1435     public static boolean shouldCollapse(Context context, boolean pendingAppLaunch) {
1436         SharedPreferences pref = getSharedPreferences(context);
1437         if(pref.getBoolean(PREF_HIDE_TASKBAR, true)) {
1438             if(!isFreeformModeEnabled(context)
1439                     || isOverridingFreeformHack(context, false))
1440                 return !LauncherHelper.getInstance().isOnHomeScreen(context);
1441             else {
1442                 FreeformHackHelper helper = FreeformHackHelper.getInstance();
1443                 if(pendingAppLaunch)
1444                     return !helper.isFreeformHackActive();
1445                 else
1446                     return !helper.isInFreeformWorkspace();
1447             }
1448         } else
1449             return false;
1450     }
1451
1452     public static boolean isOverridingFreeformHack(Context context) {
1453         return isOverridingFreeformHack(context, true);
1454     }
1455
1456     public static boolean isOverridingFreeformHack(Context context, boolean checkPref) {
1457         SharedPreferences pref = getSharedPreferences(context);
1458         return (!checkPref || isFreeformModeEnabled(context))
1459                 && ((isChromeOs(context) && (getChromeOsContextMenuFix(context)
1460                 || (pref.getBoolean(PREF_LAUNCHER, false) && launcherIsDefault(context))))
1461                 || (!isChromeOs(context) && getCurrentApiVersion() >= 28.0f)
1462                 || (isChromeOs(context) && getCurrentApiVersion() >= 30.0f));
1463     }
1464
1465     public static boolean isPlayStoreInstalled(Context context) {
1466         try {
1467             context.getPackageManager().getPackageInfo("com.android.vending", 0);
1468             return true;
1469         } catch (PackageManager.NameNotFoundException e) {
1470             return false;
1471         }
1472     }
1473
1474     public static float getCurrentApiVersion() {
1475         if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
1476             return Float.parseFloat(Build.VERSION.SDK_INT + "." + Build.VERSION.PREVIEW_SDK_INT);
1477         else
1478             return (float) Build.VERSION.SDK_INT;
1479     }
1480
1481     public static boolean hasBrokenSetLaunchBoundsApi() {
1482         return getCurrentApiVersion() >= 26.0f
1483                 && getCurrentApiVersion() < 28.0f
1484                 && !isSamsungDevice()
1485                 && !isNvidiaDevice();
1486     }
1487
1488     public static String getSecondScreenPackageName(Context context) {
1489         return getInstalledPackage(context,
1490                 "com.farmerbb.secondscreen.free",
1491                 "com.farmerbb.secondscreen");
1492     }
1493
1494     // Returns the name of an installed package from a list of package names, in order of preference
1495     private static String getInstalledPackage(Context context, String... packageNames) {
1496         return getInstalledPackage(context, Arrays.asList(packageNames));
1497     }
1498
1499     private static String getInstalledPackage(Context context, List<String> packageNames) {
1500         if(packageNames == null || packageNames.isEmpty())
1501             return null;
1502
1503         List<String> packages = packageNames instanceof ArrayList ? packageNames : new ArrayList<>(packageNames);
1504         String packageName = packages.get(0);
1505
1506         try {
1507             context.getPackageManager().getPackageInfo(packageName, 0);
1508             return packageName;
1509         } catch (PackageManager.NameNotFoundException e) {
1510             packages.remove(0);
1511             return getInstalledPackage(context, packages);
1512         }
1513     }
1514
1515     public static void showRecentAppsDialog(Context context) {
1516         Callbacks callbacks = new Callbacks();
1517         callbacks.onError = () -> showErrorDialog(context, "GET_USAGE_STATS", callbacks);
1518
1519         showRecentAppsDialog(context, callbacks);
1520     }
1521
1522     public static AlertDialog showRecentAppsDialog(Context context, Callbacks callbacks) {
1523         if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !isSystemApp(context)) {
1524             ApplicationInfo applicationInfo = null;
1525             try {
1526                 applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
1527             } catch (PackageManager.NameNotFoundException ignored) {}
1528
1529             if(applicationInfo != null) {
1530                 AppOpsManager appOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
1531                 int mode = appOpsManager.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, applicationInfo.uid, applicationInfo.packageName);
1532
1533                 if(mode != AppOpsManager.MODE_ALLOWED) {
1534                     AlertDialog.Builder builder;
1535                     if(hasAndroidTVSettings(context))
1536                         builder = buildRecentAppsDialogAndroidTV(context, callbacks);
1537                     else
1538                         builder = buildRecentAppsDialogStandard(context, callbacks);
1539
1540                     AlertDialog dialog = builder.create();
1541                     dialog.show();
1542                     dialog.setCancelable(false);
1543
1544                     return dialog;
1545                 }
1546             }
1547         }
1548
1549         callbacks.onFinish.run();
1550         return null;
1551     }
1552
1553     private static AlertDialog.Builder buildRecentAppsDialogStandard(Context context, Callbacks callbacks) {
1554         String message = context.getString(R.string.tb_enable_recent_apps, getAppName(context))
1555                 + context.getString(R.string.tb_enable_recent_apps_instructions_phone);
1556
1557         return new AlertDialog.Builder(context)
1558                 .setTitle(R.string.tb_pref_header_recent_apps)
1559                 .setMessage(message)
1560                 .setPositiveButton(R.string.tb_action_ok, (dialog, which) -> {
1561                     try {
1562                         Intent intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
1563
1564                         LauncherHelper helper = LauncherHelper.getInstance();
1565                         if(helper.isOnHomeScreen(context))
1566                             applyOpenInNewWindow(context, intent);
1567
1568                         context.startActivity(intent);
1569                         showToast(context, context.getString(R.string.tb_usage_stats_message, getAppName(context)), Toast.LENGTH_LONG);
1570
1571                         callbacks.onFinish.run();
1572                     } catch (ActivityNotFoundException e) {
1573                         callbacks.onError.run();
1574                     }
1575                 })
1576                 .setNegativeButton(R.string.tb_action_cancel, (dialog, which) -> callbacks.onFinish.run());
1577     }
1578
1579     private static AlertDialog.Builder buildRecentAppsDialogAndroidTV(Context context, Callbacks callbacks) {
1580         String message = context.getString(R.string.tb_enable_recent_apps, getAppName(context))
1581                 + context.getString(R.string.tb_enable_recent_apps_instructions_tv);
1582
1583         return new AlertDialog.Builder(context)
1584                 .setTitle(R.string.tb_pref_header_recent_apps)
1585                 .setMessage(message)
1586                 .setPositiveButton(R.string.tb_action_open_settings, (dialog, which) -> {
1587                     try {
1588                         context.startActivity(new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS));
1589                         callbacks.onFinish.run();
1590                     } catch (ActivityNotFoundException e) {
1591                         callbacks.onError.run();
1592                     }
1593                 })
1594                 .setNegativeButton(R.string.tb_action_cancel, (dialog, which) -> callbacks.onFinish.run());
1595     }
1596
1597     public static Context wrapContext(Context context) {
1598         int theme;
1599         if(isDarkTheme(context))
1600             theme = R.style.Taskbar_Dark;
1601         else
1602             theme = R.style.Taskbar;
1603
1604         return new ContextThemeWrapper(context, theme);
1605     }
1606
1607     public static boolean isPlayStoreRelease(Context context) {
1608         return isPlayStoreRelease(context, context.getPackageName());
1609     }
1610
1611     @SuppressLint("PackageManagerGetSignatures")
1612     public static boolean isPlayStoreRelease(Context context, String packageName) {
1613         Signature playStoreSignature = new Signature(context.getString(R.string.tb_signature));
1614         try {
1615             PackageManager pm = context.getPackageManager();
1616             PackageInfo info = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
1617             for(Signature signature : info.signatures) {
1618                 if(signature.equals(playStoreSignature))
1619                     return true;
1620             }
1621         } catch (Exception ignored) {}
1622
1623         return false;
1624     }
1625
1626     public static boolean isExternalAccessDisabled(Context context) {
1627         if(isLibrary(context)) return true;
1628
1629         SharedPreferences pref = getSharedPreferences(context);
1630         return !pref.getBoolean(PREF_TASKER_ENABLED, true);
1631     }
1632
1633     public static boolean enableFreeformModeShortcut(Context context) {
1634         return canEnableFreeform()
1635                 && !isOverridingFreeformHack(context, false)
1636                 && !isChromeOs(context);
1637     }
1638
1639     public static void startForegroundService(Context context, Intent intent) {
1640         if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1641             if(Settings.canDrawOverlays(context))
1642                 context.startForegroundService(intent);
1643         } else
1644             context.startService(intent);
1645     }
1646
1647     public static int getOverlayType() {
1648         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
1649                 ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
1650                 : WindowManager.LayoutParams.TYPE_PHONE;
1651     }
1652
1653     public static boolean isDelegatingHomeActivity(Context context) {
1654         Intent homeIntent = new Intent(Intent.ACTION_MAIN);
1655         homeIntent.addCategory(Intent.CATEGORY_HOME);
1656
1657         final List<ResolveInfo> listOfLaunchers = context.getPackageManager().queryIntentActivities(homeIntent, 0);
1658         for(ResolveInfo launcher : listOfLaunchers) {
1659             if(launcher.activityInfo.packageName.equals(BuildConfig.SUPPORT_APPLICATION_ID))
1660                 return true;
1661         }
1662
1663         return false;
1664     }
1665
1666     @SuppressLint("PrivateApi")
1667     public static String getSystemProperty(String key) {
1668         allowReflection();
1669         try {
1670             Class<?> cls = Class.forName("android.os.SystemProperties");
1671             return cls.getMethod("get", String.class).invoke(null, key).toString();
1672         } catch (Exception e) {
1673             return null;
1674         }
1675     }
1676
1677     public static void showImageChooser(Activity activity) {
1678         Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
1679         intent.setType("image/*");
1680         intent.addCategory(Intent.CATEGORY_OPENABLE);
1681
1682         String message = activity.getResources().getString(R.string.tb_filepicker_select_an_image_file);
1683
1684         try {
1685             activity.startActivityForResult(Intent.createChooser(intent, message), IMAGE_REQUEST_CODE);
1686         } catch (ActivityNotFoundException ex) {
1687             showToast(activity, activity.getResources().getString(R.string.tb_filepicker_install_file_manager), 50);
1688         }
1689     }
1690
1691     @SuppressWarnings("ResultOfMethodCallIgnored")
1692     public static boolean importImage(Context context, Uri uri, String filename) {
1693         try {
1694             File imagesDir = new File(context.getFilesDir(), "tb_images");
1695             imagesDir.mkdirs();
1696
1697             File importedFile = new File(imagesDir, filename + "_new");
1698             if(importedFile.exists()) importedFile.delete();
1699
1700             BufferedInputStream is = new BufferedInputStream(context.getContentResolver().openInputStream(uri));
1701             byte[] data = new byte[is.available()];
1702
1703             if(data.length > 0) {
1704                 BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(importedFile));
1705                 is.read(data);
1706                 os.write(data);
1707                 is.close();
1708                 os.close();
1709             }
1710
1711             File prevFile = new File(imagesDir, filename);
1712             if(prevFile.exists()) prevFile.delete();
1713
1714             importedFile.renameTo(prevFile);
1715             return true;
1716         } catch (IOException e) {
1717             return false;
1718         }
1719     }
1720
1721     public static String[] getImageFilenames() {
1722         return new String[] {"custom_image", "desktop_wallpaper"};
1723     }
1724
1725     public static void applyCustomImage(Context context, String filename, ImageView view, Drawable errorDrawable) {
1726         File file = new File(context.getFilesDir() + "/tb_images", filename);
1727         if(file.exists()) {
1728             Handler handler = newHandler();
1729             new Thread(() -> {
1730                 Bitmap bitmap = BitmapFactory.decodeFile(file.getPath());
1731                 handler.post(() -> {
1732                     if(bitmap != null) {
1733                         BitmapDrawable bitmapDrawable = new BitmapDrawable(context.getResources(), bitmap);
1734                         bitmapDrawable.setFilterBitmap(bitmap.getWidth() * bitmap.getHeight() > 2000);
1735                         view.setImageDrawable(bitmapDrawable);
1736                     } else {
1737                         showToastLong(context, R.string.tb_error_reading_custom_start_image);
1738                         view.setImageDrawable(errorDrawable);
1739                     }
1740                 });
1741             }).start();
1742         } else
1743             view.setImageDrawable(errorDrawable);
1744     }
1745
1746     public static String getDefaultStartButtonImage(Context context) {
1747         SharedPreferences pref = getSharedPreferences(context);
1748         return pref.getBoolean(PREF_APP_DRAWER_ICON, false)
1749                 ? PREF_START_BUTTON_IMAGE_APP_LOGO
1750                 : PREF_START_BUTTON_IMAGE_DEFAULT;
1751     }
1752
1753     private static boolean shouldLaunchTouchAbsorber(Context context) {
1754         return isOverridingFreeformHack(context) && !isChromeOs(context) && getCurrentApiVersion() < 29.0f;
1755     }
1756
1757     public static boolean isDesktopIconsEnabled(Context context) {
1758         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1
1759                 && !canBootToFreeform(context, false)
1760                 && !shouldLaunchTouchAbsorber(context);
1761     }
1762
1763     public static boolean isSystemTrayEnabled(Context context) {
1764         SharedPreferences pref = getSharedPreferences(context);
1765
1766         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
1767                 && getBooleanPrefWithDefault(context, PREF_SYS_TRAY)
1768                 && pref.getBoolean(PREF_FULL_LENGTH, true)
1769                 && !TaskbarPosition.isVertical(context);
1770     }
1771
1772     public static boolean isLibrary(Context context) {
1773         return !context.getPackageName().equals(BuildConfig.APPLICATION_ID);
1774     }
1775
1776     public static boolean applyDisplayCutoutModeTo(WindowManager.LayoutParams params) {
1777         if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
1778             params.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
1779             return true;
1780         }
1781
1782         return false;
1783     }
1784
1785     public static boolean hasAndroidTVSettings(Context context) {
1786         return getInstalledPackage(context, "com.android.tv.settings") != null
1787                 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
1788     }
1789
1790     public static void restartApp(Activity activity, boolean shouldFade) {
1791         Intent restartIntent = new Intent(activity, MainActivity.class);
1792         restartIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
1793         activity.startActivity(restartIntent);
1794
1795         activity.overridePendingTransition(
1796                 shouldFade ? android.R.anim.fade_in : 0,
1797                 shouldFade ? android.R.anim.fade_out : 0
1798         );
1799
1800         System.exit(0);
1801     }
1802
1803     public static boolean isDesktopModeSupported(Context context) {
1804         if(isLauncherPermanentlyEnabled(context) || isChromeOs(context) || isAndroidTV(context))
1805             return false;
1806
1807         return Build.VERSION.SDK_INT > Build.VERSION_CODES.P
1808                 && context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS);
1809     }
1810
1811     public static boolean isDesktopModePrefEnabled(Context context) {
1812         if(!isDesktopModeSupported(context) || !hasFreeformSupport(context))
1813             return false;
1814
1815         boolean desktopModePrefEnabled;
1816
1817         try {
1818             desktopModePrefEnabled = Settings.Global.getInt(context.getContentResolver(), "force_desktop_mode_on_external_displays") == 1;
1819         } catch (Settings.SettingNotFoundException e) {
1820             desktopModePrefEnabled = false;
1821         }
1822
1823         return desktopModePrefEnabled;
1824     }
1825
1826     public static boolean isDesktopModeActive(Context context) {
1827         return isDesktopModePrefEnabled(context) && getExternalDisplayID(context) != Display.DEFAULT_DISPLAY;
1828     }
1829
1830     private static Display getExternalDisplay(Context context) {
1831         DisplayManager dm = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
1832         Display[] displays = dm.getDisplays();
1833
1834         return displays[displays.length - 1];
1835     }
1836
1837     public static int getExternalDisplayID(Context context) {
1838         return getExternalDisplay(context).getDisplayId();
1839     }
1840
1841     public static DisplayInfo getExternalDisplayInfo(Context context) {
1842         Display display = getExternalDisplay(context);
1843         if(display == null)
1844             return new DisplayInfo(0, 0, 0, 0, false);
1845
1846         DisplayMetrics metrics = new DisplayMetrics();
1847         display.getRealMetrics(metrics);
1848
1849         int defaultDensity;
1850         try {
1851             defaultDensity = getDefaultDensity(display.getDisplayId());
1852         } catch (Exception e) {
1853             defaultDensity = 0;
1854         }
1855
1856         boolean displayDefaultsToFreeform = displayDefaultsToFreeform(context, display);
1857         return new DisplayInfo(metrics.widthPixels, metrics.heightPixels, metrics.densityDpi, defaultDensity, displayDefaultsToFreeform);
1858     }
1859
1860     @SuppressLint("PrivateApi")
1861     private static Object getWindowManagerService() throws Exception {
1862         allowReflection();
1863         return Class.forName("android.view.WindowManagerGlobal")
1864                 .getMethod("getWindowManagerService")
1865                 .invoke(null);
1866     }
1867
1868     @SuppressLint("PrivateApi")
1869     public static void setDensity(int displayID, String value) throws Exception {
1870         // From android.os.UserHandle
1871         final int USER_CURRENT_OR_SELF = -3;
1872
1873         allowReflection();
1874         if(value.equals("reset")) {
1875             Class.forName("android.view.IWindowManager")
1876                     .getMethod("clearForcedDisplayDensityForUser", int.class, int.class)
1877                     .invoke(getWindowManagerService(), displayID, USER_CURRENT_OR_SELF);
1878         } else {
1879             int density = Integer.parseInt(value);
1880
1881             Class.forName("android.view.IWindowManager")
1882                     .getMethod("setForcedDisplayDensityForUser", int.class, int.class, int.class)
1883                     .invoke(getWindowManagerService(), displayID, density, USER_CURRENT_OR_SELF);
1884         }
1885     }
1886
1887     @SuppressLint("PrivateApi")
1888     private static void setOverscan(int displayID, int value) throws Exception {
1889         allowReflection();
1890         Class.forName("android.view.IWindowManager")
1891                 .getMethod("setOverscan", int.class, int.class, int.class, int.class, int.class)
1892                 .invoke(getWindowManagerService(), displayID, 0, 0, 0, value);
1893     }
1894
1895     @SuppressLint("PrivateApi")
1896     private static Integer getDefaultDensity(int displayID) throws Exception {
1897         allowReflection();
1898         return (Integer) Class.forName("android.view.IWindowManager")
1899                 .getMethod("getInitialDisplayDensity", int.class)
1900                 .invoke(getWindowManagerService(), displayID);
1901     }
1902
1903     public static void registerReceiver(Context context, BroadcastReceiver receiver, String... actions) {
1904         unregisterReceiver(context, receiver);
1905
1906         IntentFilter intentFilter = new IntentFilter();
1907         for(String action : actions) {
1908             intentFilter.addAction(action);
1909         }
1910
1911         LocalBroadcastManager.getInstance(context).registerReceiver(receiver, intentFilter);
1912     }
1913
1914     public static void unregisterReceiver(Context context, BroadcastReceiver receiver) {
1915         LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver);
1916     }
1917
1918     public static void sendBroadcast(Context context, String action) {
1919        sendBroadcast(context, new Intent(action));
1920     }
1921
1922     public static void sendBroadcast(Context context, Intent intent) {
1923         LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
1924     }
1925
1926     public static void setComponentEnabled(Context context, Class<?> clazz, boolean enabled) {
1927         ComponentName component = new ComponentName(context, clazz);
1928         context.getPackageManager().setComponentEnabledSetting(component,
1929                 enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
1930                         : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
1931                 PackageManager.DONT_KILL_APP);
1932     }
1933
1934     public static BitmapDrawable convertToBitmapDrawable(Context context, Drawable drawable) {
1935         if(drawable instanceof BitmapDrawable)
1936             return (BitmapDrawable) drawable;
1937
1938         int width = Math.max(drawable.getIntrinsicWidth(), 1);
1939         int height = Math.max(drawable.getIntrinsicHeight(), 1);
1940
1941         Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
1942         Canvas canvas = new Canvas(bitmap);
1943
1944         drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
1945         drawable.draw(canvas);
1946
1947         return new BitmapDrawable(context.getResources(), bitmap);
1948     }
1949
1950     public static BitmapDrawable convertToMonochrome(Context context, Drawable drawable, float threshold) {
1951         Bitmap bitmap = convertToBitmapDrawable(context, drawable).getBitmap();
1952         Bitmap monoBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
1953
1954         // From https://stackoverflow.com/a/38635239
1955         float[] hsv = new float[3];
1956         for(int col = 0; col < bitmap.getWidth(); col++) {
1957             for(int row = 0; row < bitmap.getHeight(); row++) {
1958                 Color.colorToHSV(bitmap.getPixel(col, row), hsv);
1959                 if(hsv[2] > threshold) {
1960                     monoBitmap.setPixel(col, row, 0xffffffff);
1961                 } else {
1962                     monoBitmap.setPixel(col, row, 0x00000000);
1963                 }
1964             }
1965         }
1966
1967         return new BitmapDrawable(context.getResources(), monoBitmap);
1968     }
1969
1970     public static BitmapDrawable resizeDrawable(Context context, Drawable drawable, @DimenRes int iconSizeRes) {
1971         int width = Math.max(1, drawable.getIntrinsicWidth());
1972         int height = Math.max(1, drawable.getIntrinsicHeight());
1973
1974         Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
1975         Canvas canvas = new Canvas(bitmap);
1976
1977         drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
1978         drawable.draw(canvas);
1979
1980         int iconSize = context.getApplicationContext().getResources().getDimensionPixelSize(iconSizeRes);
1981         Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, true);
1982
1983         return new BitmapDrawable(context.getResources(), resizedBitmap);
1984     }
1985
1986     private static String getCurrentTheme(Context context) {
1987         String defaultTheme = context.getString(R.string.tb_pref_theme_default);
1988
1989         SharedPreferences pref = getSharedPreferences(context);
1990         String themePref = pref.getString(PREF_THEME, defaultTheme);
1991
1992         if(themePref.equals("system")) {
1993             Configuration configuration = context.getResources().getConfiguration();
1994             int currentNightMode = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK;
1995             switch(currentNightMode) {
1996                 case Configuration.UI_MODE_NIGHT_NO:
1997                     // Night mode is not active, we're using the light theme
1998                     return "light";
1999                 case Configuration.UI_MODE_NIGHT_YES:
2000                     // Night mode is active, we're using dark theme
2001                     return "dark";
2002             }
2003         } else
2004             return themePref;
2005
2006         return defaultTheme;
2007     }
2008
2009     public static boolean isFavoriteAppTilesEnabled(Context context) {
2010         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !isChromeOs(context) && !isLibrary(context);
2011     }
2012
2013     public static Intent getThemedIntent(Context context, Class<?> clazz) {
2014         Class<?> newClass;
2015
2016         if(!isDarkTheme(context))
2017             newClass = clazz;
2018         else try {
2019             newClass = Class.forName(clazz.getPackage().getName() + ".dark." + clazz.getSimpleName() + "Dark");
2020         } catch (ClassNotFoundException | NullPointerException e) {
2021             newClass = clazz;
2022         }
2023
2024         return new Intent(context, newClass);
2025     }
2026
2027     public static boolean isDarkTheme(Context context) {
2028         return getCurrentTheme(context).equals("dark");
2029     }
2030
2031     public static boolean getBooleanPrefWithDefault(Context context, String key) {
2032         context = getDisplayContext(context);
2033         int resId = getDefaultPrefResID(key, R.bool.class);
2034
2035         SharedPreferences pref = getSharedPreferences(context);
2036         boolean def = pref.getBoolean(key + "_default", context.getResources().getBoolean(resId));
2037         return pref.getBoolean(key, def);
2038     }
2039
2040     public static int getIntPrefWithDefault(Context context, String key) {
2041         context = getDisplayContext(context);
2042         int resId = getDefaultPrefResID(key, R.integer.class);
2043
2044         SharedPreferences pref = getSharedPreferences(context);
2045         int def = pref.getInt(key + "_default", context.getResources().getInteger(resId));
2046         return pref.getInt(key, def);
2047     }
2048
2049     private static int getDefaultPrefResID(String key, Class<?> rClass) {
2050         int resId;
2051
2052         try {
2053             Field field = rClass.getField("tb_def_" + key);
2054             resId = field.getInt(null);
2055         } catch (NoSuchFieldException | IllegalAccessException e) {
2056             // Pref does not have a default
2057             return 0;
2058         }
2059
2060         return resId;
2061     }
2062
2063     public static boolean isFreeformModeEnabled(Context context) {
2064         if(isLibrary(context)) return true;
2065
2066         SharedPreferences pref = getSharedPreferences(context);
2067         return pref.getBoolean(PREF_DESKTOP_MODE, false) || pref.getBoolean(PREF_FREEFORM_HACK, false);
2068     }
2069
2070     public static Context getDisplayContext(Context context) {
2071         if(isDesktopModeActive(context))
2072             return context.createDisplayContext(getExternalDisplay(context));
2073         else
2074             return context.getApplicationContext();
2075     }
2076
2077     public static int getDisplayOrientation(Context context) {
2078         return getDisplayContext(context).getResources().getConfiguration().orientation;
2079     }
2080
2081     @TargetApi(Build.VERSION_CODES.N)
2082     public static void applyOpenInNewWindow(Context context, Intent intent) {
2083         if(!isFreeformModeEnabled(context)) return;
2084
2085         intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
2086
2087         ActivityInfo activityInfo = intent.resolveActivityInfo(context.getPackageManager(), 0);
2088         if(activityInfo != null) {
2089             switch(activityInfo.launchMode) {
2090                 case ActivityInfo.LAUNCH_SINGLE_TASK:
2091                 case ActivityInfo.LAUNCH_SINGLE_INSTANCE:
2092                     intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT);
2093                     break;
2094             }
2095         }
2096     }
2097
2098     public static boolean isConsumerBuild(Context context) {
2099         return !BuildConfig.DEBUG
2100                 && !context.getPackageName().equals(BuildConfig.ANDROIDX86_APPLICATION_ID)
2101                 && !isLibrary(context);
2102     }
2103
2104     public static void allowReflection() {
2105         GlobalHelper helper = GlobalHelper.getInstance();
2106         if(helper.isReflectionAllowed()) return;
2107
2108         try {
2109             Method forName = Class.class.getDeclaredMethod("forName", String.class);
2110             Method getDeclaredMethod = Class.class.getDeclaredMethod("getDeclaredMethod", String.class, Class[].class);
2111
2112             Class<?> vmRuntimeClass = (Class<?>) forName.invoke(null, "dalvik.system.VMRuntime");
2113             Method getRuntime = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "getRuntime", null);
2114             Method setHiddenApiExemptions = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "setHiddenApiExemptions", new Class[]{String[].class});
2115
2116             Object vmRuntime = getRuntime.invoke(null);
2117             setHiddenApiExemptions.invoke(vmRuntime, new Object[]{new String[]{"L"}});
2118         } catch (Throwable ignored) {}
2119
2120         helper.setReflectionAllowed(true);
2121     }
2122
2123     public static Handler newHandler() {
2124         return new Handler(Looper.getMainLooper());
2125     }
2126
2127     public static CharSequence getAppName(Context context) {
2128         return context.getApplicationInfo().loadLabel(context.getPackageManager());
2129     }
2130
2131     public static boolean getChromeOsContextMenuFix(Context context) {
2132         if(getCurrentApiVersion() >= 30.0f) return false;
2133
2134         SharedPreferences pref = getSharedPreferences(context);
2135         return pref.getBoolean(PREF_CHROME_OS_CONTEXT_MENU_FIX, true);
2136     }
2137
2138     private static boolean displayDefaultsToFreeform(Context context, Display display) {
2139         Context dispContext = context.createDisplayContext(display);
2140         String configString = dispContext.getResources().getConfiguration().toString();
2141         return configString.contains("mDisplayWindowingMode=freeform");
2142     }
2143 }