55import android .app .usage .UsageStatsManager ;
66import android .content .Intent ;
77import android .content .pm .PackageManager ;
8+ import android .os .Build ;
89import android .os .Bundle ;
910import android .view .LayoutInflater ;
1011import android .view .View ;
2425import com .livefront .processkiller .model .ProcessDetail ;
2526import com .livefront .processkiller .task .ProcessDetailTask ;
2627
28+ import java .io .IOException ;
2729import java .lang .annotation .Retention ;
2830import java .lang .annotation .RetentionPolicy ;
2931import java .util .ArrayList ;
3234import java .util .Collections ;
3335import java .util .Comparator ;
3436import java .util .List ;
37+ import java .util .concurrent .Executor ;
38+ import java .util .concurrent .Executors ;
3539import java .util .concurrent .TimeUnit ;
3640
3741/**
@@ -87,12 +91,15 @@ public int compare(ProcessDetail lhs, ProcessDetail rhs) {
8791 private ProcessDetailTask mProcessDetailTask ;
8892 private PackageManager mPackageManager ;
8993 private List <String > mIgnoredPackages = new ArrayList <>();
94+ private Executor mExecutor = Executors .newCachedThreadPool ();
9095 private UsageStatsManager mUsageStatsManager ;
96+ private Snackbar mSnackBar = null ;
9197 private Views mViews ;
9298
9399 static class Views {
94100 RecyclerView recyclerView ;
95101 View spinner ;
102+
96103 Views (View root ) {
97104 recyclerView = (RecyclerView ) root .findViewById (R .id .recycler_view );
98105 spinner = root .findViewById (R .id .spinner );
@@ -155,6 +162,60 @@ public void onDestroyView() {
155162 }
156163 }
157164
165+ private void killProcess (@ NonNull String packageName ) {
166+ mSnackBar = Snackbar .make (
167+ mViews .recyclerView ,
168+ R .string .snackbar_process_killing ,
169+ Snackbar .LENGTH_INDEFINITE );
170+ mSnackBar .show ();
171+
172+ mExecutor .execute (() -> {
173+ sendAdbCommand ("pm grant com.livefront.processkiller "
174+ + "android.permission.WRITE_SECURE_SETTINGS "
175+ + "&> /dev/null" );
176+
177+ if (!sendAdbCommand ("devices" )) {
178+ runOnUiThread (() -> {
179+ mSnackBar .setText (R .string .snackbar_process_killing_failure_message )
180+ .setDuration (Snackbar .LENGTH_LONG )
181+ .show ();
182+ });
183+ return ;
184+ }
185+ if (!sendAdbCommand ("shell am kill " + packageName )) {
186+ runOnUiThread (() -> {
187+ mSnackBar .setText (R .string .snackbar_process_killing_failure_message )
188+ .setDuration (Snackbar .LENGTH_LONG )
189+ .show ();
190+ });
191+ return ;
192+ }
193+
194+ runOnUiThread (() -> {
195+ // We can defer the remaining logic to the legacy code
196+ killProcessLegacy (packageName );
197+ });
198+ });
199+ }
200+
201+ private void killProcessLegacy (@ NonNull String packageName ) {
202+ // This is no-op for Android 14+
203+ mActivityManager .killBackgroundProcesses (packageName );
204+
205+ if (mSnackBar == null ) {
206+ mSnackBar = Snackbar .make (
207+ mViews .recyclerView ,
208+ R .string .snackbar_process_killed_action ,
209+ Snackbar .LENGTH_LONG );
210+ }
211+ mSnackBar .setText (R .string .snackbar_process_killed_action )
212+ .setDuration (Snackbar .LENGTH_LONG )
213+ .setAction (
214+ R .string .snackbar_process_killed_action ,
215+ v -> launchIntentForPackage (packageName ))
216+ .show ();
217+ }
218+
158219 /**
159220 * Attempts to find (and launch) an intent to resume an application from its last known running
160221 * task. In most cases this should simulate clicking on the application on the Recents screen.
@@ -245,6 +306,14 @@ public void onTaskComplete(@NonNull List<ProcessDetail> processDetails) {
245306 mProcessDetailTask .execute ();
246307 }
247308
309+ private void runOnUiThread (@ NonNull Runnable runnable ) {
310+ Activity activity = getActivity ();
311+ if (activity == null ) {
312+ return ;
313+ }
314+ activity .runOnUiThread (runnable );
315+ }
316+
248317 private void setupRecyclerView () {
249318 LinearLayoutManager linearLayoutManager = new LinearLayoutManager (
250319 getActivity (),
@@ -256,24 +325,31 @@ private void setupRecyclerView() {
256325 mAdapter .setOnProcessDetailClickListener (new OnProcessDetailClickListener () {
257326 @ Override
258327 public void onProcessDetailClick (@ NonNull final ProcessDetail processDetail ) {
259- mActivityManager .killBackgroundProcesses (processDetail .getPackageName ());
260- Snackbar .make (
261- mViews .recyclerView ,
262- R .string .snackbar_process_killed_message ,
263- Snackbar .LENGTH_LONG )
264- .setAction (
265- R .string .snackbar_process_killed_action ,
266- new View .OnClickListener () {
267- @ Override
268- public void onClick (View v ) {
269- launchIntentForPackage (processDetail .getPackageName ());
270- }
271- })
272- .show ();
328+ if (Build .VERSION .SDK_INT < Build .VERSION_CODES .UPSIDE_DOWN_CAKE ) {
329+ killProcessLegacy (processDetail .getPackageName ());
330+ } else {
331+ killProcess (processDetail .getPackageName ());
332+ }
273333 }
274334 });
275335 }
276336
337+ private boolean sendAdbCommand (String command ) {
338+ Activity activity = getActivity ();
339+ if (activity == null ) {
340+ return false ;
341+ }
342+ // Note that this makes use of ADB libraries from
343+ // https://github.com/tytydraco/LADB/tree/main/app/src/main/jniLibs
344+ String adbPath = activity .getApplicationInfo ().nativeLibraryDir + "/libadb.so" ;
345+ String [] cmdLine = {"sh" , "-c" , adbPath + " " + command };
346+ try {
347+ return Runtime .getRuntime ().exec (cmdLine ).waitFor () == 0 ;
348+ } catch (IOException | InterruptedException e ) {
349+ return false ;
350+ }
351+ }
352+
277353 private void showProgress (boolean show ) {
278354 mViews .spinner .setVisibility (show ? View .VISIBLE : View .GONE );
279355 }
0 commit comments