├── settings.gradle
├── app
├── src
│ └── main
│ │ ├── assets
│ │ └── xposed_init
│ │ ├── res
│ │ ├── drawable
│ │ │ └── ic_tile.png
│ │ ├── mipmap
│ │ │ └── ic_launcher.png
│ │ └── values
│ │ │ └── strings.xml
│ │ ├── java
│ │ └── ryuunoakaihitomi
│ │ │ └── xposed
│ │ │ └── screenshothookbox
│ │ │ ├── BoolConfigIO.java
│ │ │ ├── SU.java
│ │ │ ├── QSTileSer.java
│ │ │ ├── ConfigActivity.java
│ │ │ └── X.java
│ │ └── AndroidManifest.xml
├── build.gradle
└── proguard-rules.pro
├── README.md
├── .gitignore
└── LICENSE
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/app/src/main/assets/xposed_init:
--------------------------------------------------------------------------------
1 | ryuunoakaihitomi.xposed.screenshothookbox.X
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_tile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryuunoakaihitomi/Screenshot-Hookbox/HEAD/app/src/main/res/drawable/ic_tile.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryuunoakaihitomi/Screenshot-Hookbox/HEAD/app/src/main/res/mipmap/ic_launcher.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Screenshot-Hookbox
2 |
3 | The xposed module can enhanced screenshot experience.
4 |
5 | 1.Cancel security restrictions
6 |
7 | 2.Mute
8 |
9 | 3.Reformat filename(ms+packagename)
10 |
11 | 4.0 delay
12 |
13 | 5.WEBP filetype
14 |
15 | 6.EXIF(JPEG,Android 7.0+)
16 |
17 |
18 | And a quick screenshot tile.(Not dependent on xposed.Root needed)
19 |
20 |
21 | HookPlace:[here](./app/src/main/java/ryuunoakaihitomi/xposed/screenshothookbox/X.java)
22 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 27
5 | defaultConfig {
6 | applicationId "ryuunoakaihitomi.xposed.screenshothookbox"
7 | minSdkVersion 23
8 | targetSdkVersion 27
9 | versionCode 12
10 | versionName "[10]"
11 | }
12 | buildTypes {
13 | release {
14 | minifyEnabled false
15 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
16 | }
17 | }
18 | }
19 |
20 | dependencies {
21 | compileOnly 'de.robv.android.xposed:api:82'
22 | }
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the ART/Dalvik VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 | out/
15 |
16 | # Gradle files
17 | .gradle/
18 | build/
19 |
20 | # Local configuration file (sdk path, etc)
21 | local.properties
22 |
23 | # Proguard folder generated by Eclipse
24 | proguard/
25 |
26 | # Log Files
27 | *.log
28 |
29 | # Android Studio Navigation editor temp files
30 | .navigation/
31 |
32 | # Android Studio captures folder
33 | captures/
34 |
35 | # Intellij
36 | *.iml
37 | .idea/workspace.xml
38 | .idea/tasks.xml
39 | .idea/gradle.xml
40 | .idea/dictionaries
41 | .idea/libraries
42 |
43 | # Keystore files
44 | *.jks
45 |
46 | # External native build folder generated in Android Studio 2.2 and later
47 | .externalNativeBuild
48 |
49 | # Google Services (e.g. APIs or Firebase)
50 | google-services.json
51 |
52 | # Freeline
53 | freeline.py
54 | freeline/
55 | freeline_project_description.json
56 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in C:\Users\ZQY\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 ryuunoakaihitomi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Screenshot Hookbox
3 | Screenshot
4 | The xposed module can enhanced screenshot experience.\n1.Cancel security restrictions\n2.Mute\n3.Reformat filename(ms+packagename)\n4.0 delay\n5.WEBP or JPEG filetype\n6.EXIF(JPEG,Android 7.0+)\n\nAnd a quick screenshot tile.(Not dependent on xposed.Root needed.)\nsrc: https://github.com/ryuunoakaihitomi/Screenshot-Hookbox
5 | Save to JPG
6 | Use screencap command
7 | I need root!!!
8 | Configuration Wizard
9 | Temporarily disabled Xposed
10 | Delay 1s for QSTile
11 | When the xposed mechanism is running, it is not recommended that you use this feature.
12 | $Donate //捐赠
13 | Jump to the donation page
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/java/ryuunoakaihitomi/xposed/screenshothookbox/BoolConfigIO.java:
--------------------------------------------------------------------------------
1 | package ryuunoakaihitomi.xposed.screenshothookbox;
2 |
3 | import android.content.Context;
4 | import android.os.Environment;
5 | import android.util.Log;
6 |
7 | import java.io.File;
8 | import java.io.IOException;
9 |
10 | /**
11 | * Created by ZQY on 2017/11/11.
12 | * Boolean Value I/O interface For Xposed.
13 | */
14 |
15 | class BoolConfigIO {
16 | //The hardcode for xposed. If we can, we should not do this.
17 | private static String path = Environment.getExternalStorageDirectory().getPath() + "/Android/data/ryuunoakaihitomi.xposed.screenshothookbox/files";
18 |
19 | //setter and getter.
20 | static void set(String key, boolean value) {
21 | File file = new File(path + "/" + key);
22 | if (value)
23 | try {
24 | Log.v(X.TAG, key + ",set_create_result:" + file.createNewFile());
25 | } catch (IOException e) {
26 | e.printStackTrace();
27 | }
28 | else
29 | Log.v(X.TAG, key + ",set_delete_result:" + file.delete());
30 | }
31 |
32 | static boolean get(String key) {
33 | Log.v(X.TAG, "get_path:" + path);
34 | return new File(path + "/" + key).exists();
35 | }
36 |
37 | @SuppressWarnings({"ConstantConditions", "ResultOfMethodCallIgnored"})
38 | static void init(Context context) {
39 | path = context.getExternalFilesDir(null).getPath();
40 | new File(path).mkdirs();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
31 |
34 |
35 |
40 |
41 |
42 |
43 |
44 |
45 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/app/src/main/java/ryuunoakaihitomi/xposed/screenshothookbox/SU.java:
--------------------------------------------------------------------------------
1 | package ryuunoakaihitomi.xposed.screenshothookbox;
2 |
3 | import android.content.BroadcastReceiver;
4 | import android.content.Context;
5 | import android.content.Intent;
6 | import android.util.Log;
7 |
8 | import java.io.DataOutputStream;
9 | import java.io.OutputStream;
10 |
11 | /**
12 | * Created by ZQY on 2017/10/31.
13 | * Superuser Shell Tools.
14 | */
15 |
16 | public class SU extends BroadcastReceiver {
17 |
18 | private static OutputStream os;
19 |
20 | //Execute shell in one outputstream.
21 | static void exec(String cmd) {
22 | try {
23 | if (os == null) os = Runtime.getRuntime().exec("su").getOutputStream();
24 | os.write((cmd + "\n").getBytes());
25 | os.flush();
26 | } catch (Exception e) {
27 | Log.wtf(X.TAG, "An error occurred.Please restart the app and allow it to use root permission.");
28 | e.printStackTrace();
29 | } finally {
30 | Log.d(X.TAG, "SU.exec:" + cmd);
31 | }
32 | }
33 |
34 | //UtilTool:check root permission.
35 | static synchronized boolean isRoot() {
36 | Process process = null;
37 | DataOutputStream os = null;
38 | try {
39 | process = Runtime.getRuntime().exec("su");
40 | os = new DataOutputStream(process.getOutputStream());
41 | os.writeBytes("exit\n");
42 | os.flush();
43 | int exitValue = process.waitFor();
44 | return exitValue == 0;
45 | } catch (Exception e) {
46 | return false;
47 | } finally {
48 | try {
49 | if (os != null) {
50 | os.close();
51 | }
52 | assert process != null;
53 | process.destroy();
54 | } catch (Exception e) {
55 | e.printStackTrace();
56 | }
57 | }
58 | }
59 |
60 | //AutoRun.Initialize the outputstream.
61 | @Override
62 | public void onReceive(Context context, Intent intent) {
63 | //intent.getAction() may return null.
64 | if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
65 | BoolConfigIO.init(context);
66 | if (ConfigActivity.isXposedRunning())
67 | BoolConfigIO.set(ConfigActivity.CAP, false);
68 | exec("echo init_boot");
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/app/src/main/java/ryuunoakaihitomi/xposed/screenshothookbox/QSTileSer.java:
--------------------------------------------------------------------------------
1 | package ryuunoakaihitomi.xposed.screenshothookbox;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.annotation.TargetApi;
5 | import android.content.Context;
6 | import android.os.Build;
7 | import android.os.Environment;
8 | import android.os.Handler;
9 | import android.os.Vibrator;
10 | import android.service.quicksettings.TileService;
11 |
12 | import java.io.File;
13 | import java.lang.reflect.Method;
14 | import java.text.SimpleDateFormat;
15 | import java.util.Date;
16 |
17 | /**
18 | * Created by ZQY on 2017/10/31.
19 | * Quick Setting Tile Service
20 | */
21 |
22 | @TargetApi(Build.VERSION_CODES.N)
23 | public class QSTileSer extends TileService {
24 | //Root permission request while adding the tile.
25 | @Override
26 | public void onCreate() {
27 | BoolConfigIO.init(getApplicationContext());
28 | SU.exec("echo init_service");
29 | }
30 |
31 | @Override
32 | public void onClick() {
33 | //collapse StatusBar.
34 | collapseStatusBar(this);
35 | //+1s
36 | int wait = 0;
37 | if (BoolConfigIO.get(ConfigActivity.DEL))
38 | wait = 1000;
39 | new Handler().postDelayed(new Runnable() {
40 | @Override
41 | public void run() {
42 | //shot
43 | if (BoolConfigIO.get(ConfigActivity.CAP))
44 | //Use screencap in root environment.
45 | new Handler().postDelayed(new Runnable() {
46 | @SuppressWarnings("ConstantConditions")
47 | public void run() {
48 | @SuppressLint("SimpleDateFormat") String path = new File(
49 | //Copy from AOSP src.
50 | new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "Screenshots"),
51 | String.format("Screenshot_%s.jpg",
52 | new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(System.currentTimeMillis())))).getAbsolutePath();
53 | SU.exec("screencap -j " + path);
54 | ((Vibrator) getSystemService(VIBRATOR_SERVICE)).vibrate(150);
55 | }
56 | }, 1000);
57 | else
58 | //'' Key code constant: System Request / Print Screen key.
59 | //public static final int KEYCODE_SYSRQ = 120;
60 | SU.exec("input keyevent 120");
61 | }
62 | }, wait);
63 | }
64 |
65 | @SuppressLint("PrivateApi")
66 | void collapseStatusBar(Context context) {
67 | try {
68 | //#FuckGoogle "Ensures that when parameter in a method only allows a specific set of constants, calls obey those rules."
69 | @SuppressLint("WrongConstant") Object oStatusBarManager = context.getSystemService("statusbar");
70 | Class> clzStatusBarManager = Class.forName("android.app.StatusBarManager");
71 | Method collapsePanels = clzStatusBarManager.getMethod("collapsePanels");
72 | collapsePanels.setAccessible(true);
73 | collapsePanels.invoke(oStatusBarManager);
74 | } catch (Exception e) {
75 | SU.exec("service call statusbar 2");
76 | e.printStackTrace();
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/app/src/main/java/ryuunoakaihitomi/xposed/screenshothookbox/ConfigActivity.java:
--------------------------------------------------------------------------------
1 | package ryuunoakaihitomi.xposed.screenshothookbox;
2 |
3 | import android.app.Activity;
4 | import android.app.AlertDialog;
5 | import android.content.DialogInterface;
6 | import android.content.Intent;
7 | import android.graphics.Color;
8 | import android.net.Uri;
9 | import android.os.Bundle;
10 | import android.view.WindowManager;
11 | import android.widget.Toast;
12 |
13 | /**
14 | * Created by ZQY on 2017/11/11.
15 | * Config UI
16 | */
17 |
18 | public class ConfigActivity extends Activity {
19 |
20 | //Menu
21 | static final String JPG = "isJPG";
22 | static final String CAP = "isCAP";
23 | //The Man Who Changed China
24 | static final String DEL = "XuYiMiao";
25 |
26 | //Hook entry
27 | static boolean isXposedRunning() {
28 | return false;
29 | }
30 |
31 | @Override
32 | protected void onCreate(Bundle savedInstanceState) {
33 | super.onCreate(savedInstanceState);
34 | //Clear status bar's black.
35 | getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
36 | getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
37 | getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
38 | getWindow().setStatusBarColor(Color.TRANSPARENT);
39 | BoolConfigIO.init(this);
40 | if (!SU.isRoot()) {
41 | Toast.makeText(getApplicationContext(), getString(R.string.root), Toast.LENGTH_LONG).show();
42 | finish();
43 | }
44 | new AlertDialog.Builder(ConfigActivity.this, android.R.style.Theme_Material_Dialog)
45 | .setTitle(getString(R.string.config_dialog_title))
46 | //Icon:like a notebook.
47 | .setIcon(android.R.drawable.ic_menu_agenda)
48 | .setOnCancelListener(new DialogInterface.OnCancelListener() {
49 | @Override
50 | public void onCancel(DialogInterface dialog) {
51 | finish();
52 | }
53 | })
54 | .setItems(new String[]{
55 | getRightEmoji(JPG) + getString(R.string.save_jpg),
56 | getRightEmoji(CAP) + getString(R.string.use_screencap),
57 | getRightEmoji(DEL) + getString(R.string.delay_1s),
58 | getString(R.string.donate)}, new DialogInterface.OnClickListener() {
59 | @Override
60 | public void onClick(DialogInterface dialog, int which) {
61 | switch (which) {
62 | case 0:
63 | BoolConfigIO.set(JPG, !BoolConfigIO.get(JPG));
64 | break;
65 | case 1:
66 | if (isXposedRunning())
67 | Toast.makeText(getApplicationContext(), getString(R.string.xposed), Toast.LENGTH_LONG).show();
68 | else
69 | BoolConfigIO.set(CAP, !BoolConfigIO.get(CAP));
70 | break;
71 | case 2:
72 | BoolConfigIO.set(DEL, !BoolConfigIO.get(DEL));
73 | break;
74 | case 3:
75 | Toast.makeText(getApplicationContext(), getString(R.string.donate_notice), Toast.LENGTH_SHORT).show();
76 | startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://ryuunoakaihitomi.info/donate/")));
77 | }
78 | finish();
79 | }
80 | }).show();
81 | }
82 |
83 | //As CheckBox.
84 | String getRightEmoji(String key) {
85 | return BoolConfigIO.get(key) ? "✔" : "";
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/app/src/main/java/ryuunoakaihitomi/xposed/screenshothookbox/X.java:
--------------------------------------------------------------------------------
1 | package ryuunoakaihitomi.xposed.screenshothookbox;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.app.ActivityManager;
5 | import android.app.AndroidAppHelper;
6 | import android.content.ContentValues;
7 | import android.content.Context;
8 | import android.graphics.Bitmap;
9 | import android.graphics.Point;
10 | import android.media.ExifInterface;
11 | import android.media.MediaActionSound;
12 | import android.os.Build;
13 | import android.provider.MediaStore;
14 | import android.util.Log;
15 | import android.view.Display;
16 | import android.view.WindowManager;
17 |
18 | import java.io.IOException;
19 | import java.io.OutputStream;
20 | import java.lang.reflect.Field;
21 | import java.text.SimpleDateFormat;
22 | import java.util.Date;
23 | import java.util.List;
24 |
25 | import de.robv.android.xposed.IXposedHookLoadPackage;
26 | import de.robv.android.xposed.IXposedHookZygoteInit;
27 | import de.robv.android.xposed.XC_MethodHook;
28 | import de.robv.android.xposed.XC_MethodReplacement;
29 | import de.robv.android.xposed.XposedBridge;
30 | import de.robv.android.xposed.XposedHelpers;
31 | import de.robv.android.xposed.callbacks.XC_LoadPackage;
32 |
33 | /**
34 | * Created by ZQY on 2017/10/28.
35 | * Hooker
36 | */
37 | @SuppressLint("SimpleDateFormat")
38 | public class X implements IXposedHookLoadPackage, IXposedHookZygoteInit {
39 |
40 | static final String TAG = "Screenshot_Hooklog";
41 |
42 | //_switch will come in handy if I start to hook String.format() method.
43 | private static boolean _switch;
44 |
45 | @Override
46 | public void initZygote(StartupParam startupParam) {
47 | //start.
48 | XposedBridge.log("//Logcat tag of \"Screenshot Hookbox\" is " + TAG);
49 | }
50 |
51 | @Override
52 | public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) {
53 | final String app = lpparam.packageName;
54 | //Disable the security mechanism.(1)Get the first parameter.(flag)
55 | Class> vwclz = XposedHelpers.findClass("android.view.Window", lpparam.classLoader);
56 | XposedHelpers.findAndHookMethod(vwclz, "setFlags", int.class, int.class,
57 | new XC_MethodHook() {
58 | @Override
59 | protected void beforeHookedMethod(MethodHookParam param) {
60 | int i = (Integer) param.args[0];
61 | if (i == WindowManager.LayoutParams.FLAG_SECURE) {
62 | param.args[0] = 0;
63 | Log.d(TAG, "Window.setFlags(0,):" + app);
64 | }
65 | }
66 |
67 | }
68 | );
69 | //Disable the security mechanism.(2)
70 | Class> svclz = XposedHelpers.findClass("android.view.SurfaceView", lpparam.classLoader);
71 | XposedHelpers.findAndHookMethod(svclz, "setSecure", Boolean.TYPE, new XC_MethodHook() {
72 | @Override
73 | protected void beforeHookedMethod(MethodHookParam param) {
74 | if (param.args[0].equals(true)) {
75 | param.args[0] = false;
76 | Log.d(TAG, "SurfaceView.setSecure:" + app);
77 | }
78 | }
79 | });
80 | //Check xposed status.
81 | if (app.equals(X.class.getPackage().getName())) {
82 | XposedHelpers.findAndHookMethod(ConfigActivity.class.getName(), lpparam.classLoader, "isXposedRunning", XC_MethodReplacement.returnConstant(true));
83 | }
84 | //SystemUI is the operator.
85 | if (app.equals("com.android.systemui")) {
86 | //Mute during getting screenshot.
87 | XposedHelpers.findAndHookMethod(MediaActionSound.class, "play", int.class, new XC_MethodReplacement() {
88 | @Override
89 | protected Object replaceHookedMethod(MethodHookParam param) {
90 | //The logcat shows that this is the earliest record.
91 | XposedBridge.log("A screenshot was taken.");
92 | MediaActionSound mas = (MediaActionSound) param.thisObject;
93 | mas.release();
94 | Log.d(TAG, "MediaActionSound.release");
95 | return null;
96 | }
97 | });
98 | //Reformat the fileName.(Entrance)Because there's only one place is "yyyyMMdd-HHmmss" in the whole aosp-mirror/platform_frameworks_base.
99 | XposedBridge.hookAllConstructors(SimpleDateFormat.class, new XC_MethodHook() {
100 | @Override
101 | protected void beforeHookedMethod(MethodHookParam param) {
102 | if (param.args[0].equals("yyyyMMdd-HHmmss")) {
103 | _switch = true;
104 | Log.d(TAG, "SimpleDateFormat(\"yyyyMMdd-HHmmss\")");
105 | }
106 | }
107 | });
108 | //Reformat the fileName.(Execute)(Screenshot_time(Accurate to milliseconds)_packagename.png)
109 | //It will call several times in SystemUI.
110 | XposedHelpers.findAndHookMethod(String.class, "format", String.class, Object[].class, new XC_MethodHook() {
111 | @Override
112 | protected void afterHookedMethod(MethodHookParam param) {
113 | if (_switch) {
114 | _switch = false;
115 | //Change the file type to WEBP or JPG.(filename)
116 | String fileExtension;
117 | if (BoolConfigIO.get(ConfigActivity.JPG))
118 | fileExtension = "jpg";
119 | else
120 | fileExtension = "webp";
121 | param.setResult(String.format("Screenshot_%s_%s." + fileExtension,
122 | new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss.SSS").format(new Date(System.currentTimeMillis()))
123 | , getShotObject()));
124 | Log.d(TAG, "String.format");
125 | }
126 | }
127 | });
128 | //Change the file type to WEBP or JPG.(type)
129 | XposedHelpers.findAndHookMethod(Bitmap.class, "compress", Bitmap.CompressFormat.class, int.class, OutputStream.class, new XC_MethodHook() {
130 | @Override
131 | protected void beforeHookedMethod(MethodHookParam param) {
132 | if (!BoolConfigIO.get(ConfigActivity.JPG))
133 | param.args[0] = Bitmap.CompressFormat.WEBP;
134 | else
135 | param.args[0] = Bitmap.CompressFormat.JPEG;
136 | Log.d(TAG, "Bitmap.compress");
137 | }
138 | });
139 |
140 | //Add EXIF in JPEG image.
141 | XposedHelpers.findAndHookMethod(ContentValues.class, "put", String.class, String.class, new XC_MethodHook() {
142 | @Override
143 | protected void beforeHookedMethod(MethodHookParam param) {
144 | if (param.args[0] == MediaStore.Images.ImageColumns.DATA) {
145 | Log.d(TAG, "MediaStore.Images.ImageColumns.DATA");
146 | if (BoolConfigIO.get(ConfigActivity.JPG) && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
147 | Log.d(TAG, "mImageFilePath:" + param.args[1]);
148 | final String mImageFilePath = (String) param.args[1];
149 | //Add EXIF
150 | try {
151 | Log.d(TAG, "ExifInterface.setAttribute");
152 | ExifInterface exifInterface = new ExifInterface(mImageFilePath);
153 | exifInterface.setAttribute(ExifInterface.TAG_DATETIME, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis()));
154 | //Debug:Width & Length
155 | WindowManager windowManager = (WindowManager) AndroidAppHelper.currentApplication().getSystemService(Context.WINDOW_SERVICE);
156 | Display display = windowManager.getDefaultDisplay();
157 | Point point = new Point();
158 | display.getRealSize(point);
159 | int w = point.x;
160 | int l = point.y;
161 | Log.d(TAG, "getRealSize:" + w + "*" + l);
162 | exifInterface.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, String.valueOf(w));
163 | exifInterface.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, String.valueOf(l));
164 | //Device Info
165 | exifInterface.setAttribute(ExifInterface.TAG_MAKE, Build.MANUFACTURER);
166 | exifInterface.setAttribute(ExifInterface.TAG_MODEL, Build.MODEL);
167 | exifInterface.setAttribute(ExifInterface.TAG_SOFTWARE, Build.DISPLAY);
168 | exifInterface.setAttribute(ExifInterface.TAG_MAKER_NOTE, "getShotObject:" + getShotObject());
169 | //Others
170 | exifInterface.setAttribute(ExifInterface.TAG_ARTIST, "SystemUI");
171 | exifInterface.setAttribute(ExifInterface.TAG_COPYRIGHT, "EXIF:Screenshot Hookbox");
172 | exifInterface.saveAttributes();
173 | } catch (IOException e) {
174 | e.printStackTrace();
175 | }
176 | }
177 | }
178 | }
179 | });
180 | return;
181 | }
182 | //android get the key event.As a messager.
183 | if (app.equals("android")) {
184 | //PhoneWindowManager:(at) hide
185 | Class> pwmclz = XposedHelpers.findClass("com.android.server.policy.PhoneWindowManager", lpparam.classLoader);
186 |
187 | //Set the delay waiting for screenshot to 0.
188 | XposedHelpers.findAndHookMethod(pwmclz, "getScreenshotChordLongPressDelay", XC_MethodReplacement.returnConstant(0L));
189 | }
190 | }
191 |
192 | //get the object on the top of the screen.(Top Activity's PackageName)
193 | private String getShotObject() {
194 | String ret = getLollipopRecentTask();
195 | String out;
196 | if (ret.isEmpty())
197 | out = "others";
198 | else
199 | out = ret;
200 | Log.d(TAG, "getShotObject:" + out);
201 | return out;
202 | }
203 |
204 | //Get foreground package name,it can work on lollipop or higher without PACKAGE_USAGE_STATS.(reflection.But it could't get the RecentsActivity from SystemUI.System permission needed)
205 | @SuppressWarnings({"JavaReflectionMemberAccess", "DanglingJavadoc"})
206 | private String getLollipopRecentTask() {
207 | /** @hide Process is hosting the current top activities. Note that this covers
208 | * all activities that are visible to the user. */
209 | final int PROCESS_STATE_TOP = 2;
210 | try {
211 | Field processStateField = ActivityManager.RunningAppProcessInfo.class.getDeclaredField("processState");
212 | List processes = ((ActivityManager) AndroidAppHelper.currentApplication().getSystemService(Context.ACTIVITY_SERVICE)).getRunningAppProcesses();
213 | for (ActivityManager.RunningAppProcessInfo process : processes) {
214 | if (process.importance <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND && process.importanceReasonCode == 0) {
215 | int state = processStateField.getInt(process);
216 | if (state == PROCESS_STATE_TOP) {
217 | String[] packname = process.pkgList;
218 | return packname[0];
219 | }
220 | }
221 | }
222 | } catch (Exception e) {
223 | XposedBridge.log(e);
224 | }
225 | return "";
226 | }
227 | }
228 |
--------------------------------------------------------------------------------