├── 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 | --------------------------------------------------------------------------------