fix: #171 (Shortcuts not working propertly in Android 13)

* Fix for #174 (Shortcuts not working propertly in Android 13)
* Own Launcher class and code review
This commit is contained in:
Ismael 2022-12-30 08:08:14 +01:00 committed by GitHub
parent ebd52fcca9
commit 9f6f78aa6d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 160 additions and 103 deletions

View file

@ -43,10 +43,20 @@
<activity
android:name=".RootLauncherActivity"
android:label="@string/context_action_launch_as_root"
android:exported="true" >
android:exported="true"
android:theme="@style/AppTheme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
</intent-filter>
</activity>
<activity
android:name=".ShortcutActivity"
android:exported="true"
android:theme="@style/AppTheme.NoDisplay">
<intent-filter>
<action android:name="activitylauncher.intent.action.LAUNCH_SHORTCUT"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -21,7 +21,8 @@ import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import org.thirdparty.LauncherIconCreator;
import org.thirdparty.IconCreator;
import org.thirdparty.Launcher;
import de.szalkowski.activitylauncher.databinding.FragmentAllListBinding;
@ -38,7 +39,7 @@ public class AllTasksListFragment extends Fragment implements AllTasksListAsyncP
ExpandableListAdapter adapter = parent.getExpandableListAdapter();
MyActivityInfo info = (MyActivityInfo) adapter.getChild(groupPosition, childPosition);
var rooted = isRootAllowed();
LauncherIconCreator.launchActivity(getActivity(), info.component_name, rooted && info.is_private);
Launcher.launchActivity(getActivity(), info.component_name, rooted && info.is_private);
return false;
}
);
@ -92,16 +93,16 @@ public class AllTasksListFragment extends Fragment implements AllTasksListAsyncP
MyActivityInfo activity = (MyActivityInfo) binding.expandableListView1.getExpandableListAdapter().getChild(ExpandableListView.getPackedPositionGroup(info.packedPosition), ExpandableListView.getPackedPositionChild(info.packedPosition));
switch (item.getItemId()) {
case 0:
LauncherIconCreator.createLauncherIcon(getActivity(), activity);
IconCreator.createLauncherIcon(getActivity(), activity);
break;
case 1:
RootLauncherIconCreator.createLauncherIcon(getActivity(), activity);
break;
case 2:
LauncherIconCreator.launchActivity(getActivity(), activity.component_name, false);
Launcher.launchActivity(getActivity(), activity.component_name, false);
break;
case 3:
LauncherIconCreator.launchActivity(getActivity(), activity.component_name, true);
Launcher.launchActivity(getActivity(), activity.component_name, true);
break;
case 4:
DialogFragment dialog = new ShortcutEditDialogFragment();
@ -118,7 +119,7 @@ public class AllTasksListFragment extends Fragment implements AllTasksListAsyncP
MyPackageInfo pack = (MyPackageInfo) binding.expandableListView1.getExpandableListAdapter().getGroup(ExpandableListView.getPackedPositionGroup(info.packedPosition));
switch (item.getItemId()) {
case 0:
LauncherIconCreator.createLauncherIcon(requireActivity(), pack);
IconCreator.createLauncherIcon(requireActivity(), pack);
Toast.makeText(getActivity(), getString(R.string.error_no_default_activity), Toast.LENGTH_LONG).show();
break;
case 2:

View file

@ -4,11 +4,11 @@ import android.content.ComponentName;
import android.os.Bundle;
import android.widget.Toast;
import androidx.fragment.app.FragmentActivity;
import androidx.appcompat.app.AppCompatActivity;
import org.thirdparty.LauncherIconCreator;
import org.thirdparty.Launcher;
public class RootLauncherActivity extends FragmentActivity {
public class RootLauncherActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -28,7 +28,7 @@ public class RootLauncherActivity extends FragmentActivity {
var signer = new Signer(getApplicationContext());
if (signer.validateComponentNameSignature(componentName, signature)) {
LauncherIconCreator.launchActivity(getApplicationContext(), componentName, true);
Launcher.launchActivity(getApplicationContext(), componentName, true);
}
} catch (Exception e) {
e.printStackTrace();

View file

@ -5,7 +5,7 @@ import android.content.Context;
import android.os.Bundle;
import android.widget.Toast;
import org.thirdparty.LauncherIconCreator;
import org.thirdparty.IconCreator;
public class RootLauncherIconCreator {
public static void createLauncherIcon(Context context, MyActivityInfo activity) {
@ -29,6 +29,6 @@ public class RootLauncherIconCreator {
activity.is_private = true;
activity.component_name = new ComponentName("de.szalkowski.activitylauncher", "de.szalkowski.activitylauncher.RootLauncherActivity");
LauncherIconCreator.createLauncherIcon(context, activity, extras);
IconCreator.createLauncherIcon(context, activity, extras);
}
}

View file

@ -0,0 +1,27 @@
package de.szalkowski.activitylauncher;
import static org.thirdparty.Launcher.launchActivity;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import java.net.URISyntaxException;
public class ShortcutActivity extends AppCompatActivity {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
Intent launchIntent = Intent.parseUri(getIntent().getStringExtra("extra_intent"), 0);
launchActivity(this, launchIntent.getComponent(), false);
} catch (URISyntaxException e) {
e.printStackTrace();
} finally {
finish();
}
}
}

View file

@ -12,7 +12,6 @@ import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
@ -21,7 +20,7 @@ import androidx.core.content.res.ResourcesCompat;
import androidx.fragment.app.DialogFragment;
import androidx.preference.PreferenceManager;
import org.thirdparty.LauncherIconCreator;
import org.thirdparty.IconCreator;
import java.util.Objects;
@ -134,7 +133,7 @@ public class ShortcutEditDialogFragment extends DialogFragment {
if (as_root) {
RootLauncherIconCreator.createLauncherIcon(getActivity(), ShortcutEditDialogFragment.this.activity);
} else {
LauncherIconCreator.createLauncherIcon(getActivity(), ShortcutEditDialogFragment.this.activity);
IconCreator.createLauncherIcon(getActivity(), ShortcutEditDialogFragment.this.activity);
}
})
.setNegativeButton(android.R.string.cancel, (dialog, which) -> Objects.requireNonNull(ShortcutEditDialogFragment.this.getDialog()).cancel());

View file

@ -28,24 +28,20 @@ import android.os.Build;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.szalkowski.activitylauncher.MyActivityInfo;
import de.szalkowski.activitylauncher.MyPackageInfo;
import de.szalkowski.activitylauncher.R;
public class LauncherIconCreator {
public class IconCreator {
private static Intent getActivityIntent(ComponentName activity, Bundle extras) {
private static String INTENT_LAUNCH_SHORTCUT = "activitylauncher.intent.action.LAUNCH_SHORTCUT";
public static Intent getActivityIntent(ComponentName activity, Bundle extras) {
Intent intent = new Intent();
intent.setComponent(activity);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@ -130,72 +126,6 @@ public class LauncherIconCreator {
return Icon.createWithBitmap(bmp);
}
/**
* Got reference from stackoverflow.com URL
* https://stackoverflow.com/questions/9194725/run-android-program-as-root
* https://stackoverflow.com/questions/12343227/escaping-bash-function-arguments-for-use-by-su-c
*/
public static void launchActivity(Context context, ComponentName activity, boolean asRoot) {
Intent intent = LauncherIconCreator.getActivityIntent(activity, null);
Toast.makeText(context, String.format(context.getText(R.string.starting_activity).toString(), activity.flattenToShortString()),
Toast.LENGTH_LONG).show();
try {
if (!asRoot) {
context.startActivity(intent);
} else {
startRootActivity(context, activity);
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(context, context.getText(R.string.error).toString() + ": " + e, Toast.LENGTH_LONG).show();
}
}
private static void startRootActivity(Context context, ComponentName activity) throws IOException, InterruptedException, IllegalArgumentException {
var component = activity.flattenToShortString();
boolean isValid = validateComponentName(component);
if (!isValid) {
throw new IllegalArgumentException(String.format(context.getString(R.string.exception_invalid_component_name), component));
}
Process process = Runtime.getRuntime().exec(new String[]{"su", "-c", "am start -n " + component});
String output = getProcessOutput(process);
var exitValue = process.waitFor();
if (exitValue > 0) {
throw new RuntimeException(String.format(context.getString(R.string.exception_command_error), exitValue, output));
}
}
/**
* Got reference from stackoverflow.com URL:
* https://stackoverflow.com/questions/309424/how-do-i-read-convert-an-inputstream-into-a-string-in-java
*/
@NonNull
private static String getProcessOutput(Process process) throws IOException {
var stream = process.getErrorStream();
int bufferSize = 1024;
char[] buffer = new char[bufferSize];
StringBuilder out = new StringBuilder();
Reader in = new InputStreamReader(stream, StandardCharsets.UTF_8);
for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) {
out.append(buffer, 0, numRead);
}
return out.toString();
}
/**
* In order to be on the safe side, validate component name before merging it into a root shell command
*
* @param component component name
* @return true, if valid
*/
private static boolean validateComponentName(String component) {
Pattern p = Pattern.compile("^[./a-zA-Z0-9]+$");
Matcher m = p.matcher(component);
return m.matches();
}
private static void createShortcut(Context context, String appName, Drawable draw, Intent intent, String iconResourceName) {
Toast.makeText(context, String.format(context.getText(R.string.creating_application_shortcut).toString(), appName),
@ -208,7 +138,6 @@ public class LauncherIconCreator {
}
}
@TargetApi(19)
private static void doCreateShortcut(Context context, String appName, Intent intent, String iconResourceName) {
Intent shortcutIntent = new Intent();
shortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, intent);
@ -228,12 +157,13 @@ public class LauncherIconCreator {
}
@TargetApi(26)
private static void doCreateShortcut(Context context, String appName, Drawable draw, Intent intent) {
private static void doCreateShortcut(Context context, String appName, Drawable draw, Intent extraIntent) {
ShortcutManager shortcutManager = Objects.requireNonNull(context.getSystemService(ShortcutManager.class));
if (shortcutManager.isRequestPinShortcutSupported()) {
Icon icon = getIconFromDrawable(draw);
intent.setAction(Intent.ACTION_CREATE_SHORTCUT);
Intent intent = new Intent(INTENT_LAUNCH_SHORTCUT);
intent.putExtra("extra_intent", extraIntent.toUri(0));
ShortcutInfo shortcutInfo = new ShortcutInfo.Builder(context, appName)
.setShortLabel(appName)
@ -241,8 +171,8 @@ public class LauncherIconCreator {
.setIcon(icon)
.setIntent(intent)
.build();
shortcutManager.requestPinShortcut(shortcutInfo, null);
} else {
new AlertDialog.Builder(context)
.setTitle(context.getText(R.string.error_creating_shortcut))

View file

@ -0,0 +1,83 @@
package org.thirdparty;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.szalkowski.activitylauncher.R;
public class Launcher {
/**
* Got reference from stackoverflow.com URL
* https://stackoverflow.com/questions/9194725/run-android-program-as-root
* https://stackoverflow.com/questions/12343227/escaping-bash-function-arguments-for-use-by-su-c
*/
public static void launchActivity(Context context, ComponentName activity, boolean asRoot) {
Intent intent = IconCreator.getActivityIntent(activity, null);
Toast.makeText(context, String.format(context.getText(R.string.starting_activity).toString(), activity.flattenToShortString()),
Toast.LENGTH_LONG).show();
try {
if (!asRoot) {
context.startActivity(intent);
} else {
startRootActivity(context, activity);
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(context, context.getText(R.string.error).toString() + ": " + e, Toast.LENGTH_LONG).show();
}
}
private static void startRootActivity(Context context, ComponentName activity) throws IOException, InterruptedException, IllegalArgumentException {
var component = activity.flattenToShortString();
boolean isValid = validateComponentName(component);
if (!isValid) {
throw new IllegalArgumentException(String.format(context.getString(R.string.exception_invalid_component_name), component));
}
Process process = Runtime.getRuntime().exec(new String[]{"su", "-c", "am start -n " + component});
String output = getProcessOutput(process);
var exitValue = process.waitFor();
if (exitValue > 0) {
throw new RuntimeException(String.format(context.getString(R.string.exception_command_error), exitValue, output));
}
}
/**
* Got reference from stackoverflow.com URL:
* https://stackoverflow.com/questions/309424/how-do-i-read-convert-an-inputstream-into-a-string-in-java
*/
@NonNull
private static String getProcessOutput(Process process) throws IOException {
var stream = process.getErrorStream();
int bufferSize = 1024;
char[] buffer = new char[bufferSize];
StringBuilder out = new StringBuilder();
Reader in = new InputStreamReader(stream, StandardCharsets.UTF_8);
for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) {
out.append(buffer, 0, numRead);
}
return out.toString();
}
/**
* In order to be on the safe side, validate component name before merging it into a root shell command
*
* @param component component name
* @return true, if valid
*/
private static boolean validateComponentName(String component) {
Pattern p = Pattern.compile("^[./a-zA-Z0-9]+$");
Matcher m = p.matcher(component);
return m.matches();
}
}

View file

@ -1,6 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="Theme.AppCompat.DayNight">
<item name="dialogCornerRadius">24dp</item>
</style>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="Theme.AppCompat.DayNight">
<item name="dialogCornerRadius">24dp</item>
</style>
<style name="AppTheme.NoDisplay" parent="AppTheme">
<item name="android:windowBackground">@null</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowAnimationStyle">@null</item>
<item name="android:windowDisablePreview">true</item>
<item name="android:windowNoDisplay">true</item>
</style>
</resources>

View file

@ -5,7 +5,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.2'
classpath 'com.android.tools.build:gradle:7.3.1'
}
}

View file

@ -1,6 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=f6b8596b10cce501591e92f229816aa4046424f3b24d771751b06779d58c8ec4
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists