├── .gitignore ├── .idea ├── codeStyles │ └── Project.xml ├── compiler.xml ├── dictionaries │ ├── Nlifew.xml │ └── ablist97.xml ├── encodings.xml ├── gradle.xml ├── jarRepositories.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── LICENSE ├── README.md ├── apicompat ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── android │ ├── app │ ├── ActivityThread.java │ └── Application.java │ ├── content │ ├── ClipData.java │ ├── ClipDescription.java │ ├── IClipboard.java │ └── IOnPrimaryClipChangedListener.java │ └── os │ ├── Binder.java │ ├── IBinder.java │ ├── IInterface.java │ ├── IServiceManager.java │ ├── Parcel.java │ ├── Parcelable.java │ ├── RemoteException.java │ └── ServiceManager.java ├── app ├── .gitignore ├── build.gradle ├── lib │ └── api-82.jar ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── cn │ │ └── nlifew │ │ └── clipmgr │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── QAS.xml │ │ ├── litepal.xml │ │ └── xposed_init │ ├── java │ │ └── cn │ │ │ └── nlifew │ │ │ └── clipmgr │ │ │ ├── app │ │ │ └── ThisApp.java │ │ │ ├── bean │ │ │ ├── ActionRecord.java │ │ │ ├── PackageRule.java │ │ │ └── UserRule.java │ │ │ ├── core │ │ │ ├── ClipHook.java │ │ │ ├── Helper.java │ │ │ └── XSetPrimaryClip2.java │ │ │ ├── fragment │ │ │ └── BaseFragment.java │ │ │ ├── provider │ │ │ └── ExportedProvider.java │ │ │ ├── receiver │ │ │ └── BootReceiver.java │ │ │ ├── service │ │ │ └── AliveService.java │ │ │ ├── settings │ │ │ └── Settings.java │ │ │ ├── ui │ │ │ ├── BaseActivity.java │ │ │ ├── EmptyActivity.java │ │ │ ├── about │ │ │ │ ├── AboutActivity.java │ │ │ │ ├── LoadQASTask.java │ │ │ │ ├── QAS.java │ │ │ │ ├── QASModel.java │ │ │ │ └── RecyclerAdapterImpl.java │ │ │ ├── main │ │ │ │ ├── MainActivity.java │ │ │ │ ├── MainFragment.java │ │ │ │ ├── MainViewModel.java │ │ │ │ ├── PagerAdapterImpl.java │ │ │ │ ├── record │ │ │ │ │ ├── RecordFragment.java │ │ │ │ │ ├── RecordWrapper.java │ │ │ │ │ └── RecyclerAdapterImpl.java │ │ │ │ └── rule │ │ │ │ │ ├── RecyclerAdapterImpl.java │ │ │ │ │ ├── RuleFragment.java │ │ │ │ │ └── RuleWrapper.java │ │ │ └── request │ │ │ │ ├── AlertDialogLayout.java │ │ │ │ ├── OnRequestFinishListener.java │ │ │ │ ├── RequestDialog.java │ │ │ │ └── SystemRequestDialog.java │ │ │ └── util │ │ │ ├── ClipUtils.java │ │ │ ├── DirtyUtils.java │ │ │ ├── DisplayUtils.java │ │ │ ├── PackageUtils.java │ │ │ ├── ReflectUtils.java │ │ │ └── ToastUtils.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-xhdpi │ │ └── ic_search_white_24dp.png │ │ ├── drawable-xxhdpi │ │ └── ic_search_white_24dp.png │ │ ├── drawable-xxxhdpi │ │ └── ic_search_white_24dp.png │ │ ├── drawable │ │ ├── bg_btn.xml │ │ ├── ic_launcher_background.xml │ │ └── ic_launcher_round.png │ │ ├── layout │ │ ├── activity_about.xml │ │ ├── activity_about_item.xml │ │ ├── activity_empty.xml │ │ ├── activity_main.xml │ │ ├── fragment_main.xml │ │ └── fragment_main_item.xml │ │ ├── menu │ │ └── options_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── ids.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── cn │ └── nlifew │ └── clipmgr │ └── ExampleUnitTest.java ├── build.gradle ├── cliptest.apk ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | *.keystore 15 | /magisk-module 16 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | xmlns:android 14 | 15 | ^$ 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | xmlns:.* 25 | 26 | ^$ 27 | 28 | 29 | BY_NAME 30 | 31 |
32 |
33 | 34 | 35 | 36 | .*:id 37 | 38 | http://schemas.android.com/apk/res/android 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | .*:name 48 | 49 | http://schemas.android.com/apk/res/android 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 58 | name 59 | 60 | ^$ 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | style 70 | 71 | ^$ 72 | 73 | 74 | 75 |
76 |
77 | 78 | 79 | 80 | .* 81 | 82 | ^$ 83 | 84 | 85 | BY_NAME 86 | 87 |
88 |
89 | 90 | 91 | 92 | .* 93 | 94 | http://schemas.android.com/apk/res/android 95 | 96 | 97 | ANDROID_ATTRIBUTE_ORDER 98 | 99 |
100 |
101 | 102 | 103 | 104 | .* 105 | 106 | .* 107 | 108 | 109 | BY_NAME 110 | 111 |
112 |
113 |
114 |
115 |
116 |
-------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/dictionaries/Nlifew.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | clipdata 5 | clipmgr 6 | nlifew 7 | riru 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/dictionaries/ablist97.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | xposed 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 nlifew 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ClipMgr 2 | 3 | #### 简介 4 | 这个模块通过 hook ClipboardManager.setPrimaryClip() 函数,拦截所有尝试修改剪贴板的操作。 5 | 6 | -------------------------------------------------------------------------------- /apicompat/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /apicompat/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | 3 | dependencies { 4 | implementation fileTree(dir: 'libs', include: ['*.jar']) 5 | } 6 | 7 | sourceCompatibility = "7" 8 | targetCompatibility = "7" 9 | -------------------------------------------------------------------------------- /apicompat/src/main/java/android/app/ActivityThread.java: -------------------------------------------------------------------------------- 1 | package android.app; 2 | 3 | import android.os.IBinder; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | public class ActivityThread { 9 | 10 | static final class ActivityClientRecord { 11 | // Activity activity; 12 | } 13 | 14 | // HashMap or Map or ArrayMap 15 | final Map mActivities = new HashMap<>(); 16 | 17 | public static Application currentApplication() { 18 | throw new UnsupportedOperationException("currentApplication"); 19 | } 20 | 21 | public static ActivityThread currentActivityThread() { 22 | throw new UnsupportedOperationException("currentActivityThread"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apicompat/src/main/java/android/app/Application.java: -------------------------------------------------------------------------------- 1 | package android.app; 2 | 3 | public class Application { 4 | } 5 | -------------------------------------------------------------------------------- /apicompat/src/main/java/android/content/ClipData.java: -------------------------------------------------------------------------------- 1 | package android.content; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | public class ClipData implements Parcelable { 7 | 8 | @Override 9 | public void writeToParcel(Parcel parcel, int flag) { 10 | 11 | } 12 | 13 | public static final CREATOR CREATOR = new CREATOR() { 14 | @Override 15 | public ClipData createFromParcel(Parcel source) { 16 | return null; 17 | } 18 | 19 | @Override 20 | public ClipData[] newArray(int size) { 21 | return new ClipData[0]; 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /apicompat/src/main/java/android/content/ClipDescription.java: -------------------------------------------------------------------------------- 1 | package android.content; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | public class ClipDescription implements Parcelable { 7 | 8 | @Override 9 | public void writeToParcel(Parcel parcel, int flag) { 10 | 11 | } 12 | 13 | public static final CREATOR CREATOR = new CREATOR() { 14 | @Override 15 | public ClipDescription createFromParcel(Parcel source) { 16 | return null; 17 | } 18 | 19 | @Override 20 | public ClipDescription[] newArray(int size) { 21 | return new ClipDescription[0]; 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /apicompat/src/main/java/android/content/IClipboard.java: -------------------------------------------------------------------------------- 1 | package android.content; 2 | 3 | import android.os.Binder; 4 | import android.os.IBinder; 5 | import android.os.IInterface; 6 | import android.os.Parcel; 7 | import android.os.Parcelable; 8 | import android.os.RemoteException; 9 | 10 | public interface IClipboard extends IInterface { 11 | 12 | public static abstract class Stub extends Binder implements IClipboard { 13 | 14 | private static final String DESCRIPTOR = "android.content.IClipboard"; 15 | static final int TRANSACTION_setPrimaryClip = 1; 16 | static final int TRANSACTION_clearPrimaryClip = 2; 17 | static final int TRANSACTION_getPrimaryClip = 3; 18 | static final int TRANSACTION_getPrimaryClipDescription = 4; 19 | static final int TRANSACTION_hasPrimaryClip = 5; 20 | static final int TRANSACTION_addPrimaryClipChangedListener = 6; 21 | static final int TRANSACTION_removePrimaryClipChangedListener = 7; 22 | static final int TRANSACTION_hasClipboardText = 8; 23 | 24 | private static class Proxy implements IClipboard { 25 | private final IBinder mRemote; 26 | 27 | private Proxy(IBinder iBinder) { 28 | this.mRemote = iBinder; 29 | } 30 | 31 | @Override 32 | public IBinder asBinder() { 33 | return this.mRemote; 34 | } 35 | 36 | public String getInterfaceDescriptor() { 37 | return Stub.DESCRIPTOR; 38 | } 39 | 40 | public void setPrimaryClip(ClipData clipData, String str) throws RemoteException { 41 | Parcel obtain = Parcel.obtain(); 42 | Parcel obtain2 = Parcel.obtain(); 43 | try { 44 | obtain.writeInterfaceToken(Stub.DESCRIPTOR); 45 | if (clipData != null) { 46 | obtain.writeInt(1); 47 | clipData.writeToParcel(obtain, 0); 48 | } else { 49 | obtain.writeInt(0); 50 | } 51 | obtain.writeString(str); 52 | this.mRemote.transact(TRANSACTION_setPrimaryClip, 53 | obtain, obtain2, 0); 54 | obtain2.readException(); 55 | } finally { 56 | obtain2.recycle(); 57 | obtain.recycle(); 58 | } 59 | } 60 | 61 | public void clearPrimaryClip(String str) throws RemoteException { 62 | Parcel obtain = Parcel.obtain(); 63 | Parcel obtain2 = Parcel.obtain(); 64 | try { 65 | obtain.writeInterfaceToken(Stub.DESCRIPTOR); 66 | obtain.writeString(str); 67 | this.mRemote.transact(TRANSACTION_clearPrimaryClip, 68 | obtain, obtain2, 0); 69 | obtain2.readException(); 70 | } finally { 71 | obtain2.recycle(); 72 | obtain.recycle(); 73 | } 74 | } 75 | 76 | public ClipData getPrimaryClip(String str) throws RemoteException { 77 | ClipData clipData; 78 | Parcel obtain = Parcel.obtain(); 79 | Parcel obtain2 = Parcel.obtain(); 80 | try { 81 | obtain.writeInterfaceToken(Stub.DESCRIPTOR); 82 | obtain.writeString(str); 83 | this.mRemote.transact(TRANSACTION_getPrimaryClip, 84 | obtain, obtain2, 0); 85 | obtain2.readException(); 86 | if (obtain2.readInt() != 0) { 87 | clipData = (ClipData) ClipData.CREATOR.createFromParcel(obtain2); 88 | } else { 89 | clipData = null; 90 | } 91 | return clipData; 92 | } finally { 93 | obtain2.recycle(); 94 | obtain.recycle(); 95 | } 96 | } 97 | 98 | public ClipDescription getPrimaryClipDescription(String str) throws RemoteException { 99 | ClipDescription clipDescription; 100 | Parcel obtain = Parcel.obtain(); 101 | Parcel obtain2 = Parcel.obtain(); 102 | try { 103 | obtain.writeInterfaceToken(Stub.DESCRIPTOR); 104 | obtain.writeString(str); 105 | this.mRemote.transact(TRANSACTION_getPrimaryClipDescription, 106 | obtain, obtain2, 0); 107 | obtain2.readException(); 108 | if (obtain2.readInt() != 0) { 109 | clipDescription = ClipDescription.CREATOR.createFromParcel(obtain2); 110 | } else { 111 | clipDescription = null; 112 | } 113 | return clipDescription; 114 | } finally { 115 | obtain2.recycle(); 116 | obtain.recycle(); 117 | } 118 | } 119 | 120 | public boolean hasPrimaryClip(String str) throws RemoteException { 121 | Parcel obtain = Parcel.obtain(); 122 | Parcel obtain2 = Parcel.obtain(); 123 | try { 124 | obtain.writeInterfaceToken(Stub.DESCRIPTOR); 125 | obtain.writeString(str); 126 | boolean z = false; 127 | this.mRemote.transact(TRANSACTION_hasPrimaryClip, 128 | obtain, obtain2, 0); 129 | obtain2.readException(); 130 | if (obtain2.readInt() != 0) { 131 | z = true; 132 | } 133 | return z; 134 | } finally { 135 | obtain2.recycle(); 136 | obtain.recycle(); 137 | } 138 | } 139 | 140 | public void addPrimaryClipChangedListener(IOnPrimaryClipChangedListener iOnPrimaryClipChangedListener, String str) throws RemoteException { 141 | Parcel obtain = Parcel.obtain(); 142 | Parcel obtain2 = Parcel.obtain(); 143 | try { 144 | obtain.writeInterfaceToken(Stub.DESCRIPTOR); 145 | obtain.writeStrongBinder(iOnPrimaryClipChangedListener != null ? 146 | iOnPrimaryClipChangedListener.asBinder() : 147 | null); 148 | obtain.writeString(str); 149 | this.mRemote.transact(TRANSACTION_addPrimaryClipChangedListener, 150 | obtain, obtain2, 0); 151 | obtain2.readException(); 152 | } finally { 153 | obtain2.recycle(); 154 | obtain.recycle(); 155 | } 156 | } 157 | 158 | public void removePrimaryClipChangedListener(IOnPrimaryClipChangedListener iOnPrimaryClipChangedListener) throws RemoteException { 159 | Parcel obtain = Parcel.obtain(); 160 | Parcel obtain2 = Parcel.obtain(); 161 | try { 162 | obtain.writeInterfaceToken(Stub.DESCRIPTOR); 163 | obtain.writeStrongBinder(iOnPrimaryClipChangedListener != null ? 164 | iOnPrimaryClipChangedListener.asBinder() : 165 | null); 166 | this.mRemote.transact(TRANSACTION_removePrimaryClipChangedListener, 167 | obtain, obtain2, 0); 168 | obtain2.readException(); 169 | } finally { 170 | obtain2.recycle(); 171 | obtain.recycle(); 172 | } 173 | } 174 | 175 | public boolean hasClipboardText(String str) throws RemoteException { 176 | Parcel obtain = Parcel.obtain(); 177 | Parcel obtain2 = Parcel.obtain(); 178 | try { 179 | obtain.writeInterfaceToken(Stub.DESCRIPTOR); 180 | obtain.writeString(str); 181 | boolean z = false; 182 | this.mRemote.transact(TRANSACTION_hasClipboardText, 183 | obtain, obtain2, 0); 184 | obtain2.readException(); 185 | if (obtain2.readInt() != 0) { 186 | z = true; 187 | } 188 | return z; 189 | } finally { 190 | obtain2.recycle(); 191 | obtain.recycle(); 192 | } 193 | } 194 | } 195 | 196 | public Stub() { 197 | attachInterface(this, DESCRIPTOR); 198 | } 199 | 200 | public static IClipboard asInterface(IBinder iBinder) { 201 | if (iBinder == null) { 202 | return null; 203 | } 204 | IInterface queryLocalInterface = iBinder.queryLocalInterface(DESCRIPTOR); 205 | if (queryLocalInterface == null || !(queryLocalInterface instanceof IClipboard)) { 206 | return new Proxy(iBinder); 207 | } 208 | return (IClipboard) queryLocalInterface; 209 | } 210 | 211 | @Override 212 | public IBinder asBinder() { 213 | return this; 214 | } 215 | 216 | @Override 217 | public boolean onTransact(int i, Parcel parcel, Parcel parcel2, int i2) throws RemoteException { 218 | ClipData clipData; 219 | String str = DESCRIPTOR; 220 | if (i != 1598968902) { 221 | switch (i) { 222 | case TRANSACTION_setPrimaryClip: 223 | parcel.enforceInterface(str); 224 | if (parcel.readInt() != 0) { 225 | clipData = ClipData.CREATOR.createFromParcel(parcel); 226 | } else { 227 | clipData = null; 228 | } 229 | setPrimaryClip(clipData, parcel.readString()); 230 | parcel2.writeNoException(); 231 | return true; 232 | case TRANSACTION_clearPrimaryClip: 233 | parcel.enforceInterface(str); 234 | clearPrimaryClip(parcel.readString()); 235 | parcel2.writeNoException(); 236 | return true; 237 | case TRANSACTION_getPrimaryClip: 238 | parcel.enforceInterface(str); 239 | ClipData primaryClip = getPrimaryClip(parcel.readString()); 240 | parcel2.writeNoException(); 241 | if (primaryClip != null) { 242 | parcel2.writeInt(1); 243 | primaryClip.writeToParcel(parcel2, 1); 244 | } else { 245 | parcel2.writeInt(0); 246 | } 247 | return true; 248 | case TRANSACTION_getPrimaryClipDescription: 249 | parcel.enforceInterface(str); 250 | ClipDescription primaryClipDescription = getPrimaryClipDescription(parcel.readString()); 251 | parcel2.writeNoException(); 252 | if (primaryClipDescription != null) { 253 | parcel2.writeInt(1); 254 | primaryClipDescription.writeToParcel(parcel2, 1); 255 | } else { 256 | parcel2.writeInt(0); 257 | } 258 | return true; 259 | case TRANSACTION_hasPrimaryClip: 260 | parcel.enforceInterface(str); 261 | boolean hasPrimaryClip = hasPrimaryClip(parcel.readString()); 262 | parcel2.writeNoException(); 263 | parcel2.writeInt(hasPrimaryClip ? 1 : 0); 264 | return true; 265 | case TRANSACTION_addPrimaryClipChangedListener: 266 | parcel.enforceInterface(str); 267 | addPrimaryClipChangedListener(IOnPrimaryClipChangedListener.Stub.asInterface(parcel.readStrongBinder()), parcel.readString()); 268 | parcel2.writeNoException(); 269 | return true; 270 | case TRANSACTION_removePrimaryClipChangedListener: 271 | parcel.enforceInterface(str); 272 | removePrimaryClipChangedListener(IOnPrimaryClipChangedListener.Stub.asInterface(parcel.readStrongBinder())); 273 | parcel2.writeNoException(); 274 | return true; 275 | case TRANSACTION_hasClipboardText: 276 | parcel.enforceInterface(str); 277 | boolean hasClipboardText = hasClipboardText(parcel.readString()); 278 | parcel2.writeNoException(); 279 | parcel2.writeInt(hasClipboardText ? 1 : 0); 280 | return true; 281 | default: 282 | return super.onTransact(i, parcel, parcel2, i2); 283 | } 284 | } else { 285 | parcel2.writeString(str); 286 | return true; 287 | } 288 | } 289 | } 290 | 291 | void addPrimaryClipChangedListener(IOnPrimaryClipChangedListener iOnPrimaryClipChangedListener, String str) throws RemoteException; 292 | 293 | void clearPrimaryClip(String str) throws RemoteException; 294 | 295 | ClipData getPrimaryClip(String str) throws RemoteException; 296 | 297 | ClipDescription getPrimaryClipDescription(String str) throws RemoteException; 298 | 299 | boolean hasClipboardText(String str) throws RemoteException; 300 | 301 | boolean hasPrimaryClip(String str) throws RemoteException; 302 | 303 | void removePrimaryClipChangedListener(IOnPrimaryClipChangedListener iOnPrimaryClipChangedListener) throws RemoteException; 304 | 305 | void setPrimaryClip(ClipData clipData, String str) throws RemoteException; 306 | } 307 | -------------------------------------------------------------------------------- /apicompat/src/main/java/android/content/IOnPrimaryClipChangedListener.java: -------------------------------------------------------------------------------- 1 | package android.content; 2 | 3 | import android.os.Binder; 4 | import android.os.IBinder; 5 | import android.os.IInterface; 6 | import android.os.Parcel; 7 | import android.os.RemoteException; 8 | 9 | public interface IOnPrimaryClipChangedListener extends IInterface { 10 | 11 | public static abstract class Stub extends Binder implements IOnPrimaryClipChangedListener { 12 | 13 | private static final String DESCRIPTOR = "android.content.IOnPrimaryClipChangedListener"; 14 | static final int TRANSACTION_dispatchPrimaryClipChanged = 1; 15 | 16 | private static class Proxy implements IOnPrimaryClipChangedListener { 17 | private final IBinder mRemote; 18 | 19 | Proxy(IBinder iBinder) { 20 | this.mRemote = iBinder; 21 | } 22 | 23 | public IBinder asBinder() { 24 | return this.mRemote; 25 | } 26 | 27 | public String getInterfaceDescriptor() { 28 | return Stub.DESCRIPTOR; 29 | } 30 | 31 | public void dispatchPrimaryClipChanged() throws RemoteException { 32 | Parcel obtain = Parcel.obtain(); 33 | try { 34 | obtain.writeInterfaceToken(Stub.DESCRIPTOR); 35 | this.mRemote.transact(TRANSACTION_dispatchPrimaryClipChanged, obtain, null, 1); 36 | } finally { 37 | obtain.recycle(); 38 | } 39 | } 40 | } 41 | 42 | public Stub() { 43 | attachInterface(this, DESCRIPTOR); 44 | } 45 | 46 | public static IOnPrimaryClipChangedListener asInterface(IBinder iBinder) { 47 | if (iBinder == null) { 48 | return null; 49 | } 50 | IInterface queryLocalInterface = iBinder.queryLocalInterface(DESCRIPTOR); 51 | if (queryLocalInterface == null || !(queryLocalInterface instanceof IOnPrimaryClipChangedListener)) { 52 | return new Proxy(iBinder); 53 | } 54 | return (IOnPrimaryClipChangedListener) queryLocalInterface; 55 | } 56 | 57 | @Override 58 | public IBinder asBinder() { 59 | return this; 60 | } 61 | 62 | public boolean onTransact(int i, Parcel parcel, Parcel parcel2, int i2) throws RemoteException { 63 | String str = DESCRIPTOR; 64 | if (i == TRANSACTION_dispatchPrimaryClipChanged) { 65 | parcel.enforceInterface(str); 66 | dispatchPrimaryClipChanged(); 67 | return true; 68 | } else if (i != 1598968902) { 69 | return super.onTransact(i, parcel, parcel2, i2); 70 | } else { 71 | parcel2.writeString(str); 72 | return true; 73 | } 74 | } 75 | 76 | } 77 | 78 | void dispatchPrimaryClipChanged() throws RemoteException; 79 | } 80 | -------------------------------------------------------------------------------- /apicompat/src/main/java/android/os/Binder.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | public class Binder implements IBinder { 4 | 5 | public void attachInterface(IInterface i, String s) { 6 | 7 | } 8 | 9 | @Override 10 | public IInterface queryLocalInterface(String s) { 11 | return null; 12 | } 13 | 14 | @Override 15 | public boolean transact(int code, Parcel param, Parcel result, int flag) { 16 | return false; 17 | } 18 | 19 | @Override 20 | public boolean onTransact(int code, Parcel param, Parcel result, int flag) throws RemoteException { 21 | return false; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apicompat/src/main/java/android/os/IBinder.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | public interface IBinder { 4 | int FIRST_CALL_TRANSACTION = 0x00000001; 5 | int LAST_CALL_TRANSACTION = 0x00ffffff; 6 | int INTERFACE_TRANSACTION = ('_'<<24)|('N'<<16)|('T'<<8)|'F'; 7 | 8 | boolean transact(int code, Parcel param, Parcel result, int flag); 9 | boolean onTransact(int code, Parcel param, Parcel result, int flag) throws RemoteException; 10 | IInterface queryLocalInterface(String s); 11 | } 12 | -------------------------------------------------------------------------------- /apicompat/src/main/java/android/os/IInterface.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | public interface IInterface { 4 | 5 | IBinder asBinder(); 6 | } 7 | -------------------------------------------------------------------------------- /apicompat/src/main/java/android/os/IServiceManager.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | public interface IServiceManager extends IInterface { 4 | } 5 | -------------------------------------------------------------------------------- /apicompat/src/main/java/android/os/Parcel.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | public class Parcel { 4 | 5 | public static Parcel obtain() { 6 | return new Parcel(); 7 | } 8 | 9 | public void writeInterfaceToken(String s) { 10 | 11 | } 12 | 13 | public void writeInt(int value) { 14 | 15 | } 16 | 17 | public void writeString(String value) { 18 | 19 | } 20 | 21 | public void writeStrongBinder(IBinder binder) { 22 | 23 | } 24 | 25 | public void writeNoException() { 26 | 27 | } 28 | 29 | public int readInt() { 30 | return 0; 31 | } 32 | 33 | public String readString() { 34 | return null; 35 | } 36 | 37 | public IBinder readStrongBinder() { 38 | return null; 39 | } 40 | 41 | public void readException() { 42 | 43 | } 44 | 45 | public void enforceInterface(String s) { 46 | 47 | } 48 | 49 | public void recycle() { 50 | 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /apicompat/src/main/java/android/os/Parcelable.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | public interface Parcelable { 4 | 5 | void writeToParcel(Parcel parcel, int flag); 6 | 7 | interface CREATOR { 8 | T createFromParcel(Parcel source); 9 | T[] newArray(int size); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apicompat/src/main/java/android/os/RemoteException.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | public class RemoteException extends Exception { 4 | } 5 | -------------------------------------------------------------------------------- /apicompat/src/main/java/android/os/ServiceManager.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | import java.util.HashMap; 4 | 5 | public final class ServiceManager { 6 | 7 | private static IServiceManager sServiceManager; 8 | 9 | private static IServiceManager getIServiceManager() { 10 | throw new UnsupportedOperationException("getIServiceManager"); 11 | } 12 | 13 | 14 | private static HashMap sCache = new HashMap(); 15 | 16 | public static IBinder getService(String name) { 17 | throw new UnsupportedOperationException("getService"); 18 | } 19 | 20 | public static void addService(String name, IBinder service) { 21 | throw new UnsupportedOperationException("addService"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release 3 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion rootProject.ext.compileSdkVersion 5 | buildToolsVersion rootProject.ext.buildToolsVersion 6 | defaultConfig { 7 | applicationId "cn.nlifew.clipmgr" 8 | minSdkVersion rootProject.ext.minSdkVersion 9 | targetSdkVersion rootProject.ext.targetSdkVersion 10 | versionCode rootProject.ext.versionCode 11 | versionName rootProject.ext.versionName 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | 15 | 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | multiDexEnabled false 20 | // shrinkResources true 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | } 30 | 31 | dependencies { 32 | implementation fileTree(dir: 'libs', include: ['*.jar']) 33 | compileOnly fileTree(dir: 'lib', include: ['*.jar']) 34 | compileOnly project(':apicompat') 35 | 36 | implementation 'androidx.appcompat:appcompat:1.2.0' 37 | implementation 'com.google.android.material:material:1.2.1' 38 | implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' 39 | implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' 40 | implementation 'androidx.annotation:annotation:1.1.0' 41 | 42 | implementation 'org.litepal.android:java:3.0.0' 43 | 44 | 45 | testImplementation 'junit:junit:4.12' 46 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 47 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 48 | } 49 | -------------------------------------------------------------------------------- /app/lib/api-82.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nlifew/ClipMgr/6d78437995b7c12551f8fd9156eeccb001b3cfea/app/lib/api-82.jar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | 2 | ############################################# 3 | # 4 | # 对于一些基本指令的添加 5 | # 6 | ############################################# 7 | # 代码混淆压缩比,在0~7之间,默认为5,一般不做修改 8 | -optimizationpasses 5 9 | 10 | # 混合时不使用大小写混合,混合后的类名为小写 11 | -dontusemixedcaseclassnames 12 | 13 | # 指定不去忽略非公共库的类 14 | -dontskipnonpubliclibraryclasses 15 | 16 | # 这句话能够使我们的项目混淆后产生映射文件 17 | # 包含有类名->混淆后类名的映射关系 18 | -verbose 19 | 20 | # 指定不去忽略非公共库的类成员 21 | -dontskipnonpubliclibraryclassmembers 22 | 23 | # 不做预校验,preverify是proguard的四个步骤之一,Android不需要preverify,去掉这一步能够加快混淆速度。 24 | -dontpreverify 25 | 26 | # 保留Annotation不混淆 27 | -keepattributes *Annotation*,InnerClasses 28 | 29 | # 避免混淆泛型 30 | -keepattributes Signature 31 | 32 | # 抛出异常时保留代码行号 33 | -keepattributes SourceFile,LineNumberTable 34 | 35 | # 指定混淆是采用的算法,后面的参数是一个过滤器 36 | # 这个过滤器是谷歌推荐的算法,一般不做更改 37 | -optimizations !code/simplification/cast,!field/*,!class/merging/* 38 | 39 | ############################################# 40 | # 41 | # Android开发中一些需要保留的公共部分 start 42 | # 43 | ############################################# 44 | 45 | # 保留我们使用的四大组件,自定义的Application等等这些类不被混淆 46 | # 因为这些子类都有可能被外部调用 47 | -keep public class * extends android.app.Activity 48 | -keep public class * extends android.app.Appliction 49 | -keep public class * extends android.app.Service 50 | -keep public class * extends android.content.BroadcastReceiver 51 | -keep public class * extends android.content.ContentProvider 52 | -keep public class * extends android.app.backup.BackupAgentHelper 53 | -keep public class * extends android.preference.Preference 54 | -keep public class * extends android.view.View 55 | -keep public class com.android.vending.licensing.ILicensingService 56 | 57 | # 保留support下的所有类及其内部类 58 | -keep class android.support.** {*;} 59 | 60 | # 保留继承的 61 | -keep public class * extends android.support.v4.** 62 | -keep public class * extends android.support.v7.** 63 | -keep public class * extends android.support.annotation.** 64 | 65 | # 保留R下面的资源 66 | -keep class **.R$* {*;} 67 | 68 | # 保留本地native方法不被混淆 69 | -keepclasseswithmembernames class * { 70 | native ; 71 | } 72 | 73 | # 保留在Activity中的方法参数是view的方法, 74 | # 这样以来我们在layout中写的onClick就不会被影响 75 | -keepclassmembers class * extends android.app.Activity{ 76 | public void *(android.view.View); 77 | } 78 | 79 | # 保留枚举类不被混淆 80 | -keepclassmembers enum * { 81 | public static **[] values(); 82 | public static ** valueOf(java.lang.String); 83 | } 84 | 85 | # 保留我们自定义控件(继承自View)不被混淆 86 | -keep public class * extends android.view.View{ 87 | *** get*(); 88 | void set*(***); 89 | public (android.content.Context); 90 | public (android.content.Context, android.util.AttributeSet); 91 | public (android.content.Context, android.util.AttributeSet, int); 92 | } 93 | 94 | # 保留Parcelable序列化类不被混淆 95 | -keep class * implements android.os.Parcelable { 96 | public static final android.os.Parcelable$Creator *; 97 | } 98 | 99 | # 保留Serializable序列化的类不被混淆 100 | -keepclassmembers class * implements java.io.Serializable { 101 | static final long serialVersionUID; 102 | private static final java.io.ObjectStreamField[] serialPersistentFields; 103 | !static !transient ; 104 | !private ; 105 | !private ; 106 | private void writeObject(java.io.ObjectOutputStream); 107 | private void readObject(java.io.ObjectInputStream); 108 | java.lang.Object writeReplace(); 109 | java.lang.Object readResolve(); 110 | } 111 | 112 | # 对于带有回调函数的onXXEvent、**On*Listener的,不能被混淆 113 | -keepclassmembers class * { 114 | void *(**On*Event); 115 | void *(**On*Listener); 116 | } 117 | 118 | # webView处理,项目中没有使用到webView忽略即可 119 | -keepclassmembers class fqcn.of.javascript.interface.for.webview { 120 | public *; 121 | } 122 | -keepclassmembers class * extends android.webkit.webViewClient { 123 | public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap); 124 | public boolean *(android.webkit.WebView, java.lang.String); 125 | } 126 | -keepclassmembers class * extends android.webkit.webViewClient { 127 | public void *(android.webkit.webView, jav.lang.String); 128 | } 129 | -keepclassmembers class com.lieyunwang.finance.activity.InfoNewDetailActivity.InJavaScriptLocalObj{ 130 | public *; 131 | } 132 | -keepattributes *JavascriptInterface* 133 | 134 | -keepclassmembers class * { 135 | public (org.json.JSONObject); 136 | } 137 | 138 | -keep public class com.xxx.xxx.R$*{ 139 | public static final int *; 140 | } 141 | 142 | # 自定义类的混淆 143 | # keep annotated by NotProguard ---- like this: keep class com.example.test.xxxBean {*;} 144 | # ---- like this: keep class com.example.test.** {*;} 145 | # 下面这块主要针对实体类处理,后面如果有地方需要添加一些不被混淆的变量,函数都可以通过添加@NotProguard来注解 146 | # com.xxx.xxx修改为实际的包名 147 | #-keep @com.xxx.xxx.anotation.NotProguard class * {*;} 148 | #-keep class * { 149 | #@com.xxx.xxx.anotation.NotProguard ; 150 | #} 151 | #-keepclassmembers class * { 152 | #@com.xxx.xxx.anotation.NotProguard ; 153 | #} 154 | 155 | # Tablayout反射修改下划线宽度导致的tabtrip空指针问题 - 需要可打开 156 | #-keep class android.support.design.widget.TabLayout{*;} 157 | 158 | #(可选)避免Log打印输出 159 | #-assumenosideeffects class android.util.Log { 160 | # public static *** v(...); 161 | # public static *** d(...); 162 | # public static *** i(...); 163 | # public static *** w(...); 164 | # } 165 | 166 | ############################################# 167 | # 168 | # 以上是Android基本混淆规则 end 169 | # 170 | ############################################# 171 | 172 | ############################################# 173 | # 174 | # 第三方混淆 start 175 | # 176 | ############################################# 177 | 178 | 179 | -keep class org.litepal.** { *; } 180 | -keep class * extends org.litepal.crud.LitePalSupport { *; } 181 | -keep class * extends org.litepal.LitePalApplication {*;} 182 | -keep class * extends de.robv.android.xposed.** {*;} 183 | 184 | ############################################# 185 | # 186 | # 第三方混淆 end 187 | # 188 | ############################################# 189 | 190 | -------------------------------------------------------------------------------- /app/src/androidTest/java/cn/nlifew/clipmgr/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr; 2 | 3 | import android.support.test.InstrumentationRegistry; 4 | import android.support.test.runner.AndroidJUnit4; 5 | 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * @see Testing documentation 13 | */ 14 | @RunWith(AndroidJUnit4.class) 15 | public class ExampleInstrumentedTest { 16 | @Test 17 | public void useAppContext() { 18 | // Context of the app under test. 19 | Context appContext = InstrumentationRegistry.getTargetContext(); 20 | 21 | assertEquals("cn.nlifew.clipmgr", appContext.getPackageName()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 19 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 58 | 59 | 60 | 61 | 62 | 63 | 66 | 69 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /app/src/main/assets/QAS.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 当我们拦截到剪贴板修改时,这个 app 和我们的管理器运行在不同的进程里。 5 | 此时需要查询这个应用的访问规则(允许、拒绝还是询问),并将这次操作记录保存都我们自己的数据库, 6 | 这就需要进程间通讯——目前的实现方案是使用 ContentProvider 方案。然而在不同的系统上这个行为无法保证, 7 | 某些对后台限制比较严格的 ROM 会禁止这一行为。因此我们不得不开启一个前台服务,以保证链接正常 8 | 9 | 10 | 11 | 新版本添加了两个权限,分别是 "开启前台服务" 权限和 "监听开机广播" 权限。原因同第一条。 12 | 13 | 14 | 15 | 这个说来话长。如果是老用户应该知道,最开始的几个版本是在本 app 内弹窗的,换句话说就是有 app 修改剪贴板, 16 | 我们将拦截这一行为并转发到我们自己的 app,然后弹窗询问什么的。然而对于某些应用或 ROM 并不合适, 17 | 比如 QQ,MIUI 等,它会自作聪明地弹出一个对话框——"即将打开 放开我的剪贴板,是否允许 ?",一度让我很无语。 18 | 而且同样存在后台问题,如果弹窗失败(并没有办法得知是否弹窗成功),用户将丢失所有的剪贴板操作,这是不可接受的。 19 | 在之前两个版本,我将弹窗放到要拦截的宿主进程内。看起来解决了对不对?实际上引入了一个巨大的隐患——兼容性问题。 20 | 不同 app 间的 theme 不同,弹框的样式根本无法做到统一。前几天我尝试硬编码一个 View 代替自带的 dialog 样式, 21 | 您猜怎么着?哈哈,微信还是那个鬼样子,即使 setTextColor(Color.BLACK),人家仍然是白色的。 22 | 23 | 气急败坏,恼羞成怒的我终于决定放弃这些"轻量级"的方案,不如让系统代替我们弹窗。换句话说,我会在 Android 24 | 刚刚启动时把剪贴板服务 hook 住,等需要弹的时候让系统帮我们弹。 25 | 26 | 27 | 28 | 这个原因有很多,可能是 xposed 不正常,还有可能是厂商修改了 IClipboard 接口(比如 setPrimaryClip() 的函数签名), 29 | 这都会导致拦截失败/不工作。 30 | 还需要注意的一点是:某些 app,如 OneNote 等,除了使用系统剪贴板外还维护了一个自己的"剪贴板实例", 31 | 这个"剪贴板"只针对它自己有用,不会对全局剪贴板造成污染,也没法拦截。 32 | 33 | 34 | 35 | 您现在已经尝试自己解决问题了,不愧是您!请记住,最有效的办法就是使用 xposed manager 自带的"日志"功能。 36 | 请先清空所有日志,然后重启一次,立即进行可疑操作以复现操作,然后回到 xposed manager,重新加载日志并保存, 然后找开发者反馈。 37 | 38 | 39 | 40 | 后台死了,参考第一点 41 | 42 | 43 | 44 | 后台死了,数据库链接不正常,参考第一点 45 | 46 | 47 | 48 | 爆裂吧现实,崩坏吧精神,Banishment This WORLD !!! 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/assets/litepal.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/assets/xposed_init: -------------------------------------------------------------------------------- 1 | cn.nlifew.clipmgr.core.ClipHook 2 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/app/ThisApp.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.app; 2 | 3 | 4 | 5 | import android.app.Application; 6 | import android.content.Context; 7 | import android.os.Handler; 8 | import android.os.Looper; 9 | 10 | import androidx.annotation.Keep; 11 | 12 | import org.litepal.LitePalApplication; 13 | 14 | @Keep 15 | public class ThisApp extends LitePalApplication { 16 | private static final String TAG = "ThisApp"; 17 | 18 | public static final Handler mH = new Handler(Looper.getMainLooper()); 19 | 20 | public static Application currentApplication; 21 | 22 | @Override 23 | protected void attachBaseContext(Context base) { 24 | super.attachBaseContext(base); 25 | currentApplication = this; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/bean/ActionRecord.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.bean; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import org.litepal.crud.LitePalSupport; 7 | 8 | public final class ActionRecord extends LitePalSupport implements Parcelable { 9 | public static final class Column { 10 | public static final String PACKAGE = "pkg"; 11 | public static final String TIME = "time"; 12 | public static final String APP_NAME = "appName"; 13 | public static final String TEXT = "text"; 14 | public static final String ACTION = "action"; 15 | } 16 | 17 | public static final class Table { 18 | public static final String NAME = "ActionRecord"; 19 | } 20 | 21 | 22 | public static final int ACTION_DENY = 1; 23 | public static final int ACTION_GRANT = 0; 24 | 25 | private String pkg; 26 | private String appName; 27 | private long time; 28 | private String text; 29 | private int action; 30 | 31 | public ActionRecord(String name, String pkg, String text, int action) { 32 | this.appName = name; 33 | this.pkg = pkg; 34 | this.text = text; 35 | this.action = action; 36 | this.time = System.currentTimeMillis(); 37 | } 38 | 39 | public String getPkg() { 40 | return pkg; 41 | } 42 | 43 | public void setPkg(String pkg) { 44 | this.pkg = pkg; 45 | } 46 | 47 | public long getTime() { 48 | return time; 49 | } 50 | 51 | public void setTime(long time) { 52 | this.time = time; 53 | } 54 | 55 | public String getText() { 56 | return text; 57 | } 58 | 59 | public void setText(String text) { 60 | this.text = text; 61 | } 62 | 63 | public int getAction() { 64 | return action; 65 | } 66 | 67 | public void setAction(int action) { 68 | this.action = action; 69 | } 70 | 71 | 72 | public String getAppName() { 73 | return appName; 74 | } 75 | 76 | public void setAppName(String appName) { 77 | this.appName = appName; 78 | } 79 | 80 | @Override 81 | public int describeContents() { 82 | return 0; 83 | } 84 | 85 | @Override 86 | public void writeToParcel(Parcel dest, int flags) { 87 | dest.writeString(this.pkg); 88 | dest.writeLong(this.time); 89 | dest.writeString(this.text); 90 | dest.writeInt(this.action); 91 | dest.writeString(this.appName); 92 | } 93 | 94 | public ActionRecord() { 95 | } 96 | 97 | private ActionRecord(Parcel in) { 98 | this.pkg = in.readString(); 99 | this.time = in.readLong(); 100 | this.text = in.readString(); 101 | this.action = in.readInt(); 102 | this.appName = in.readString(); 103 | } 104 | 105 | public static final Creator CREATOR = new Creator() { 106 | @Override 107 | public ActionRecord createFromParcel(Parcel source) { 108 | return new ActionRecord(source); 109 | } 110 | 111 | @Override 112 | public ActionRecord[] newArray(int size) { 113 | return new ActionRecord[size]; 114 | } 115 | }; 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/bean/PackageRule.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.bean; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import org.litepal.crud.LitePalSupport; 7 | 8 | public final class PackageRule extends LitePalSupport implements Parcelable { 9 | public static final int RULE_REQUEST = 0; 10 | public static final int RULE_GRANT = 1; 11 | public static final int RULE_DENY = 2; 12 | 13 | public static final class Column { 14 | public static final String PACKAGE = "pkg"; 15 | public static final String RULE = "rule"; 16 | } 17 | 18 | public static final class Table { 19 | public static final String NAME = "PackageRule"; 20 | } 21 | 22 | public PackageRule(String pkg, int rule) { 23 | this.pkg = pkg; 24 | this.rule = rule; 25 | } 26 | 27 | private String pkg; 28 | private int rule; 29 | 30 | public String getPkg() { 31 | return pkg; 32 | } 33 | 34 | public void setPkg(String pkg) { 35 | this.pkg = pkg; 36 | } 37 | 38 | public int getRule() { 39 | return rule; 40 | } 41 | 42 | public void setRule(int rule) { 43 | this.rule = rule; 44 | } 45 | 46 | 47 | @Override 48 | public int describeContents() { 49 | return 0; 50 | } 51 | 52 | @Override 53 | public void writeToParcel(Parcel dest, int flags) { 54 | dest.writeString(this.pkg); 55 | dest.writeInt(this.rule); 56 | } 57 | 58 | public PackageRule() { 59 | } 60 | 61 | private PackageRule(Parcel in) { 62 | this.pkg = in.readString(); 63 | this.rule = in.readInt(); 64 | } 65 | 66 | public static final Creator CREATOR = new Creator() { 67 | @Override 68 | public PackageRule createFromParcel(Parcel source) { 69 | return new PackageRule(source); 70 | } 71 | 72 | @Override 73 | public PackageRule[] newArray(int size) { 74 | return new PackageRule[size]; 75 | } 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/bean/UserRule.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.bean; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import org.litepal.LitePal; 7 | import org.litepal.crud.LitePalSupport; 8 | 9 | /** 10 | * @Deprecated use PackageRule instead 11 | */ 12 | @Deprecated 13 | public class UserRule extends LitePalSupport implements Parcelable { 14 | public static final int RULE_GRANT = 1; 15 | public static final int RULE_DENY = -1; 16 | public static final int RULE_REQUIRE = 0; 17 | public static final int RULE_DEFAULT = RULE_REQUIRE; 18 | 19 | public static int findPackageRule(String pkg) { 20 | UserRule rule = LitePal 21 | .where("pkg = ?", pkg) 22 | .findFirst(UserRule.class); 23 | return rule == null ? RULE_REQUIRE : rule.flag; 24 | } 25 | 26 | public static void savePackageRule(String pkg, int rule) { 27 | UserRule old = LitePal 28 | .where("pkg = ?", pkg) 29 | .findFirst(UserRule.class); 30 | if (old == null) { 31 | old = new UserRule(pkg); 32 | } 33 | old.flag = rule; 34 | old.save(); 35 | } 36 | 37 | private String pkg; 38 | private int flag; 39 | 40 | 41 | public UserRule(String pkg) { 42 | this.pkg = pkg; 43 | } 44 | 45 | public String getPkg() { 46 | return pkg; 47 | } 48 | 49 | public void setPkg(String pkg) { 50 | this.pkg = pkg; 51 | } 52 | 53 | public int getFlag() { 54 | return flag; 55 | } 56 | 57 | public void setFlag(int flag) { 58 | this.flag = flag; 59 | } 60 | 61 | 62 | @Override 63 | public int describeContents() { 64 | return 0; 65 | } 66 | 67 | @Override 68 | public void writeToParcel(Parcel dest, int flags) { 69 | dest.writeString(this.pkg); 70 | dest.writeInt(this.flag); 71 | } 72 | 73 | public UserRule() { 74 | } 75 | 76 | public UserRule(String pkg, int rule) { 77 | this.pkg = pkg; 78 | this.flag = rule; 79 | } 80 | 81 | private UserRule(Parcel in) { 82 | this.pkg = in.readString(); 83 | this.flag = in.readInt(); 84 | } 85 | 86 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 87 | @Override 88 | public UserRule createFromParcel(Parcel source) { 89 | return new UserRule(source); 90 | } 91 | 92 | @Override 93 | public UserRule[] newArray(int size) { 94 | return new UserRule[size]; 95 | } 96 | }; 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/core/ClipHook.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.core; 2 | 3 | import android.content.ClipData; 4 | import android.content.Context; 5 | import android.os.IBinder; 6 | import android.os.ServiceManager; 7 | import android.system.Os; 8 | 9 | import java.lang.reflect.Method; 10 | import java.lang.reflect.Modifier; 11 | import java.util.Arrays; 12 | import java.util.Objects; 13 | 14 | import de.robv.android.xposed.IXposedHookLoadPackage; 15 | import de.robv.android.xposed.XC_MethodHook; 16 | import de.robv.android.xposed.XposedBridge; 17 | import de.robv.android.xposed.XposedHelpers; 18 | import de.robv.android.xposed.callbacks.XC_LoadPackage; 19 | 20 | public class ClipHook implements IXposedHookLoadPackage { 21 | private static final String TAG = "ClipHook"; 22 | private static final String SYSTEM_SERVER = "android"; 23 | 24 | @Override 25 | public void handleLoadPackage(XC_LoadPackage.LoadPackageParam param) throws Throwable { 26 | XposedBridge.log(TAG + " package: " + param.packageName + 27 | " pid: " + Os.getpid()); 28 | 29 | if (Objects.equals(SYSTEM_SERVER, param.packageName)) { 30 | registerRequestDialogService(); 31 | } 32 | } 33 | 34 | private static void registerRequestDialogService() { 35 | XposedBridge.log(TAG + " [" + Os.getuid() + ", " + Os.getpid() + "]"); 36 | 37 | XposedBridge.hookAllMethods(ServiceManager.class, 38 | "addService", new XC_MethodHook() { 39 | 40 | @Override 41 | protected void afterHookedMethod(MethodHookParam param) throws Throwable { 42 | final String name = (String) param.args[0]; 43 | final IBinder service = (IBinder) param.args[1]; 44 | 45 | if (! Objects.equals(Context.CLIPBOARD_SERVICE, name)) { 46 | return; 47 | } 48 | 49 | Method method = findAndCheckMethod(service); 50 | if (method == null) { 51 | XposedBridge.log(TAG + ": registerRequestDialogService: " + 52 | "missing setPrimaryClip(ClipData, String, ...)"); 53 | return; 54 | } 55 | 56 | Class[] oldParams = method.getParameterTypes(); 57 | int length = oldParams.length; 58 | 59 | Object[] newParams = new Object[length + 1]; 60 | System.arraycopy(oldParams, 0, newParams, 0, length); 61 | newParams[length] = new XSetPrimaryClip2(method, service); 62 | 63 | XposedHelpers.findAndHookMethod(service.getClass(), 64 | "setPrimaryClip", newParams); 65 | 66 | XposedBridge.log(TAG + ": registerRequestDialogService: done"); 67 | } 68 | }); 69 | } 70 | 71 | private static Method findAndCheckMethod(IBinder service) { 72 | for (Method method : service.getClass().getDeclaredMethods()) { 73 | if ((method.getModifiers() & Modifier.PUBLIC) == 0) { 74 | continue; 75 | } 76 | if (! "setPrimaryClip".equals(method.getName())) { 77 | continue; 78 | } 79 | 80 | Class[] params = method.getParameterTypes(); 81 | Class result = method.getReturnType(); 82 | 83 | if (params.length >= 2 && result == void.class 84 | && params[0] == ClipData.class && params[1] == String.class) { 85 | return method; 86 | } 87 | } 88 | return null; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/core/Helper.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.core; 2 | 3 | import android.content.ClipData; 4 | import android.content.ContentResolver; 5 | import android.content.ContentValues; 6 | import android.content.Context; 7 | import android.content.pm.ApplicationInfo; 8 | import android.content.pm.PackageManager; 9 | import android.database.Cursor; 10 | import android.net.Uri; 11 | import android.os.Binder; 12 | import android.os.IBinder; 13 | import android.os.Parcel; 14 | 15 | import cn.nlifew.clipmgr.bean.ActionRecord; 16 | import cn.nlifew.clipmgr.bean.PackageRule; 17 | import cn.nlifew.clipmgr.provider.ExportedProvider; 18 | import cn.nlifew.clipmgr.util.ClipUtils; 19 | import de.robv.android.xposed.XposedBridge; 20 | 21 | final class Helper { 22 | private static final String TAG = "Helper"; 23 | 24 | private Helper() { } 25 | 26 | 27 | static int getPackageRule(Context context, String packageName) { 28 | Cursor cursor = null; 29 | try { 30 | Uri uri = Uri.parse("content://" + ExportedProvider.AUTHORITY 31 | + "/" + ExportedProvider.PATH_PACKAGE_RULE 32 | + "/" + packageName); 33 | cursor = context.getContentResolver().query(uri, null, 34 | null, null, null); 35 | if (cursor != null && cursor.moveToNext()) { 36 | return cursor.getInt(cursor.getColumnIndex(PackageRule.Column.RULE)); 37 | } 38 | } catch (Exception e) { 39 | XposedBridge.log(TAG + ": getPackageRule: failed to query rule of " + packageName); 40 | XposedBridge.log(e); 41 | } finally { 42 | if (cursor != null) { 43 | cursor.close(); 44 | } 45 | } 46 | return PackageRule.RULE_REQUEST; 47 | } 48 | 49 | static void saveActionRecord(Context context, String packageName, 50 | ClipData clipData, int action) { 51 | 52 | ContentValues values = new ContentValues(); 53 | values.put(ActionRecord.Column.PACKAGE, packageName); 54 | values.put(ActionRecord.Column.ACTION, action); 55 | values.put(ActionRecord.Column.TEXT, ClipUtils.clip2String(clipData)); 56 | values.put(ActionRecord.Column.TIME, System.currentTimeMillis()); 57 | values.put(ActionRecord.Column.APP_NAME, packageName); 58 | 59 | try { 60 | PackageManager pm = context.getPackageManager(); 61 | ApplicationInfo info = pm.getApplicationInfo(packageName, 0); 62 | values.put(ActionRecord.Column.APP_NAME, info.loadLabel(pm).toString()); 63 | 64 | Uri uri = Uri.parse("content://" + ExportedProvider.AUTHORITY 65 | + "/" + ExportedProvider.PATH_ACTION_RECORD); 66 | context.getContentResolver().insert(uri, values); 67 | } catch (Exception e) { 68 | XposedBridge.log(TAG + ": saveActionRecord: failed to insert ActionRecord: " + 69 | packageName + ", " + ClipUtils.clip2String(clipData) + ", " + action); 70 | XposedBridge.log(e); 71 | } 72 | } 73 | 74 | static void savePackageRule(Context context, String packageName, int rule) { 75 | ContentResolver resolver = context.getContentResolver(); 76 | 77 | try { 78 | Uri uri = Uri.parse("content://" + ExportedProvider.AUTHORITY 79 | + "/" + ExportedProvider.PATH_PACKAGE_RULE 80 | + "/" + packageName); 81 | resolver.delete(uri, null, null); 82 | 83 | ContentValues values = new ContentValues(); 84 | values.put(PackageRule.Column.PACKAGE, packageName); 85 | values.put(PackageRule.Column.RULE, rule); 86 | 87 | resolver.insert(uri, values); 88 | } catch (Exception e) { 89 | XposedBridge.log(TAG + ": savePackageRule: failed to save the rule [" + 90 | rule + "] of package: [" + packageName + "]"); 91 | XposedBridge.log(e); 92 | } 93 | } 94 | 95 | static String clip2SimpleText(ClipData clipData) { 96 | StringBuilder sb = new StringBuilder(64) 97 | .append("尝试修改剪贴板为:"); 98 | ClipUtils.clip2SimpleString(clipData, sb); 99 | return sb.toString(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/core/XSetPrimaryClip2.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.core; 2 | 3 | import android.app.ActivityThread; 4 | import android.content.ClipData; 5 | import android.content.Context; 6 | import android.content.DialogInterface; 7 | import android.content.IClipboard; 8 | import android.content.pm.ApplicationInfo; 9 | import android.content.pm.PackageManager; 10 | import android.os.Binder; 11 | import android.os.Handler; 12 | import android.os.IBinder; 13 | import android.os.Looper; 14 | 15 | import androidx.core.content.ContextCompat; 16 | 17 | import java.lang.reflect.Method; 18 | import java.lang.reflect.Modifier; 19 | import java.util.ArrayList; 20 | import java.util.Arrays; 21 | import java.util.Objects; 22 | 23 | import cn.nlifew.clipmgr.BuildConfig; 24 | import cn.nlifew.clipmgr.bean.ActionRecord; 25 | import cn.nlifew.clipmgr.bean.PackageRule; 26 | import cn.nlifew.clipmgr.ui.request.OnRequestFinishListener; 27 | import cn.nlifew.clipmgr.ui.request.SystemRequestDialog; 28 | import de.robv.android.xposed.XC_MethodHook; 29 | import de.robv.android.xposed.XposedBridge; 30 | 31 | import static cn.nlifew.clipmgr.core.Helper.clip2SimpleText; 32 | import static cn.nlifew.clipmgr.core.Helper.savePackageRule; 33 | import static cn.nlifew.clipmgr.ui.request.OnRequestFinishListener.RESULT_POSITIVE; 34 | import static cn.nlifew.clipmgr.ui.request.OnRequestFinishListener.RESULT_REMEMBER; 35 | import static cn.nlifew.clipmgr.util.PackageUtils.getApplicationInfo; 36 | import static cn.nlifew.clipmgr.core.Helper.getPackageRule; 37 | import static cn.nlifew.clipmgr.core.Helper.saveActionRecord; 38 | 39 | final class XSetPrimaryClip2 extends XC_MethodHook { 40 | private static final String TAG = "XSetPrimaryClip2"; 41 | 42 | XSetPrimaryClip2(Method method, IBinder service) { 43 | mThis = service; 44 | mSetPrimaryClip = method; 45 | } 46 | 47 | 48 | private final Object mThis; 49 | private final Method mSetPrimaryClip; 50 | 51 | private SystemRequestDialog mRequestDialog; 52 | 53 | private final Handler mH = new Handler(Looper.getMainLooper()); 54 | private final ArrayList mPendingQueue 55 | = new ArrayList<>(4); // is better than LinkedList ? 56 | 57 | 58 | private boolean shouldIgnoreThisCall(Object[] args) { 59 | // 保证不会递归拦截 60 | if (mPendingQueue.size() > 0 && Arrays.equals(mPendingQueue.get(0).args, args)) { 61 | return true; 62 | } 63 | 64 | // 不能是我们自己 65 | if (BuildConfig.APPLICATION_ID.equals(args[1])) { 66 | return true; 67 | } 68 | 69 | return false; 70 | } 71 | 72 | @Override 73 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable { 74 | final long id = Binder.clearCallingIdentity(); 75 | 76 | try { 77 | if (shouldIgnoreThisCall(param.args)) { 78 | return; 79 | } 80 | 81 | final ClipData clipData = (ClipData) param.args[0]; 82 | final String packageName = (String) param.args[1]; 83 | 84 | final Context context = ActivityThread.currentApplication(); 85 | final int rule = getPackageRule(context, packageName); 86 | 87 | XposedBridge.log(TAG + ": the rule of " + packageName + " is " + rule); 88 | 89 | switch (rule) { 90 | case PackageRule.RULE_GRANT: // 授权访问剪贴板 91 | saveActionRecord(context, packageName, clipData, ActionRecord.ACTION_GRANT); 92 | break; 93 | case PackageRule.RULE_DENY: // 禁止访问剪贴板 94 | saveActionRecord(context, packageName, clipData, ActionRecord.ACTION_DENY); 95 | param.setResult(null); 96 | break; 97 | case PackageRule.RULE_REQUEST: // 弹出授权对话框 98 | param.setResult(null); 99 | 100 | PendingTransaction pt = new PendingTransaction(mSetPrimaryClip, mThis, param.args); 101 | pt.context = context; 102 | pt.packageName = packageName; 103 | pt.clipData = clipData; 104 | pt.identity = id; 105 | mH.post(() -> showRequestDialog(pt)); 106 | break; 107 | } 108 | } finally { 109 | Binder.restoreCallingIdentity(id); 110 | } 111 | } 112 | 113 | private void showRequestDialog(PendingTransaction pending) { 114 | // 如果正在弹窗,只需要加进任务队列 115 | if (mRequestDialog != null) { 116 | // 遍历当前队列,找到包名相同的任务,删掉它 117 | int old = -1; 118 | 119 | for (int i = 0, n = mPendingQueue.size(); i < n; i++) { 120 | PendingTransaction it = mPendingQueue.get(i); 121 | if (Objects.equals(it.packageName, pending.packageName)) { 122 | old = i; 123 | mPendingQueue.remove(old); 124 | break; 125 | } 126 | } 127 | mPendingQueue.add(pending); 128 | if (old == 0) { 129 | ClipData clipData = pending.clipData; 130 | mRequestDialog.setMessage(clip2SimpleText(clipData)); 131 | } 132 | return; 133 | } 134 | 135 | mPendingQueue.add(pending); 136 | 137 | mRequestDialog = new SystemRequestDialog(pending.context); 138 | applyParam(mRequestDialog, pending); 139 | mRequestDialog.show(); 140 | } 141 | 142 | private void applyParam(SystemRequestDialog dialog, PendingTransaction pending) { 143 | final Context context = pending.context; 144 | final String packageName = pending.packageName; 145 | final ClipData clipData = pending.clipData; 146 | 147 | PackageManager pm = context.getPackageManager(); 148 | ApplicationInfo info = getApplicationInfo(pm, packageName); 149 | 150 | CallbackImpl callback = new CallbackImpl(); 151 | 152 | dialog.setTitle(info != null ? info.loadLabel(pm) : 153 | context.getString(android.R.string.unknownName)); 154 | dialog.setIcon(info != null ? info.loadIcon(pm) : 155 | ContextCompat.getDrawable(context, android.R.drawable.sym_def_app_icon)); 156 | 157 | dialog.setCancelable(false); 158 | dialog.setMessage(clip2SimpleText(clipData)); 159 | dialog.setButton(DialogInterface.BUTTON_POSITIVE, "允许", callback); 160 | dialog.setButton(DialogInterface.BUTTON_NEGATIVE, "拒绝", callback); 161 | dialog.setOnDismissListener(callback); // [1] 162 | 163 | // [1] 看起来很有问题,很多余对不对 ? 为什么明明已经设置了 cancelable = false 164 | // 还要多此一举再设置个回调 ? 天真,你是不知道有个模块叫 "对话框取消",呵呵 165 | // 只要一手贱按下返回键,好家伙,以后全不能复制了 166 | } 167 | 168 | private static final class PendingTransaction { 169 | private static final String TAG = "PendingTransaction"; 170 | 171 | public PendingTransaction(Method method, Object obj, Object[] args) { 172 | this.method = method; 173 | this.object = obj; 174 | this.args = args; 175 | } 176 | 177 | final Method method; 178 | final Object object; 179 | final Object[] args; 180 | 181 | long identity; 182 | 183 | Context context; 184 | String packageName; 185 | ClipData clipData; 186 | 187 | void transact() { 188 | try { 189 | method.invoke(object, args); 190 | } catch (Throwable t) { 191 | XposedBridge.log(TAG + ": transact: failed"); 192 | XposedBridge.log(t); 193 | } 194 | } 195 | } 196 | 197 | private class CallbackImpl extends SystemRequestDialog.Callback { 198 | private static final String TAG = "CallbackImpl"; 199 | 200 | @Override 201 | public void onRequestFinish(int result) { 202 | final long id = Binder.clearCallingIdentity(); 203 | try { 204 | handleResult(result); 205 | } catch (Throwable t) { 206 | XposedBridge.log(TAG + ": onRequestFinish: failed"); 207 | XposedBridge.log(t); 208 | } finally { 209 | Binder.restoreCallingIdentity(id); 210 | } 211 | 212 | // 从队列中移除 213 | mRequestDialog = null; 214 | mPendingQueue.remove(0); 215 | 216 | // 可能其它 app 也需要修改剪贴板 217 | if (mPendingQueue.size() != 0) { 218 | PendingTransaction p = mPendingQueue.remove(0); 219 | mH.post(() -> showRequestDialog(p)); 220 | } 221 | } 222 | 223 | private void handleResult(int result) { 224 | XposedBridge.log(TAG + ": handleResult: received result: " + result); 225 | 226 | // 先不要移除,用来防止递归拦截 227 | final PendingTransaction pending = mPendingQueue.get(0); 228 | 229 | final int packageRule; 230 | final Context context = pending.context; 231 | final String packageName = pending.packageName; 232 | final ClipData clipData = pending.clipData; 233 | 234 | if ((result & RESULT_POSITIVE) != 0) { // 允许访问剪贴板 235 | packageRule = PackageRule.RULE_GRANT; 236 | saveActionRecord(context, packageName, clipData, ActionRecord.ACTION_GRANT); 237 | Binder.restoreCallingIdentity(pending.identity); 238 | pending.transact(); 239 | } 240 | else { // 只要没有明确允许,就是拒绝 241 | packageRule = PackageRule.RULE_DENY; 242 | saveActionRecord(context, packageName, clipData, ActionRecord.ACTION_DENY); 243 | } 244 | 245 | if ((result & RESULT_REMEMBER) != 0) { 246 | savePackageRule(context, packageName, packageRule); 247 | } 248 | 249 | XposedBridge.log(TAG + ": handleResult: done"); 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/fragment/BaseFragment.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.fragment; 2 | 3 | import androidx.fragment.app.Fragment; 4 | 5 | public abstract class BaseFragment extends Fragment { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/provider/ExportedProvider.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.provider; 2 | 3 | import android.content.ContentProvider; 4 | import android.content.ContentValues; 5 | import android.content.UriMatcher; 6 | import android.database.Cursor; 7 | import android.database.MatrixCursor; 8 | import android.net.Uri; 9 | import android.util.Log; 10 | 11 | import androidx.annotation.NonNull; 12 | 13 | import org.litepal.LitePal; 14 | 15 | import java.util.Map; 16 | 17 | import cn.nlifew.clipmgr.app.ThisApp; 18 | import cn.nlifew.clipmgr.bean.ActionRecord; 19 | import cn.nlifew.clipmgr.bean.PackageRule; 20 | import cn.nlifew.clipmgr.settings.Settings; 21 | import cn.nlifew.clipmgr.util.ClipUtils; 22 | import cn.nlifew.clipmgr.util.ToastUtils; 23 | 24 | public class ExportedProvider extends ContentProvider { 25 | private static final String TAG = "ExportedProvider"; 26 | public static final String AUTHORITY = "cn.nlifew.clipmgr.provider"; 27 | 28 | public static final String PATH_PACKAGE_RULE = "rule"; 29 | public static final String PATH_ACTION_RECORD = "record"; 30 | 31 | private static final int TABLE_PACKAGE_RULE_DIR = 1; 32 | private static final int TABLE_PACKAGE_RULE_ITEM = 2; 33 | private static final int TABLE_ACTION_RECORD_DIR = 3; 34 | private static final int TABLE_ACTION_RECORD_ITEM = 4; 35 | 36 | private static final UriMatcher sUriMatcher; 37 | static { 38 | sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 39 | sUriMatcher.addURI(AUTHORITY, PATH_PACKAGE_RULE, TABLE_PACKAGE_RULE_DIR); 40 | sUriMatcher.addURI(AUTHORITY, PATH_PACKAGE_RULE + "/*", TABLE_PACKAGE_RULE_ITEM); 41 | sUriMatcher.addURI(AUTHORITY, PATH_ACTION_RECORD, TABLE_ACTION_RECORD_DIR); 42 | sUriMatcher.addURI(AUTHORITY, PATH_ACTION_RECORD + "/*", TABLE_ACTION_RECORD_ITEM); 43 | } 44 | 45 | @Override 46 | public boolean onCreate() { 47 | return true; 48 | } 49 | 50 | @Override 51 | public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { 52 | Log.i(TAG, "delete: " + uri); 53 | switch (sUriMatcher.match(uri)) { 54 | case TABLE_PACKAGE_RULE_DIR: 55 | return LitePal.getDatabase().delete(PackageRule.Table.NAME, selection, selectionArgs); 56 | case TABLE_PACKAGE_RULE_ITEM: 57 | return LitePal.getDatabase().delete(PackageRule.Table.NAME, 58 | PackageRule.Column.PACKAGE + "= ?", 59 | new String[]{uri.getLastPathSegment()}); 60 | } 61 | return 0; 62 | } 63 | 64 | @Override 65 | public String getType(@NonNull Uri uri) { 66 | Log.i(TAG, "getType: " + uri); 67 | switch (sUriMatcher.match(uri)) { 68 | case TABLE_PACKAGE_RULE_DIR: 69 | return "vnd.android.cursor.dir/vnd." + AUTHORITY + "." + PATH_PACKAGE_RULE; 70 | case TABLE_PACKAGE_RULE_ITEM: 71 | return "vnd.android.cursor.item/vnd." + AUTHORITY + "." + PATH_PACKAGE_RULE; 72 | case TABLE_ACTION_RECORD_DIR: 73 | return "vnd.android.cursor.dir/vnd." + AUTHORITY + "." + PATH_ACTION_RECORD; 74 | case TABLE_ACTION_RECORD_ITEM: 75 | return "vnd.android.cursor.item/vnd." + AUTHORITY + "." + PATH_ACTION_RECORD; 76 | } 77 | return null; 78 | } 79 | 80 | @Override 81 | public Uri insert(@NonNull Uri uri, ContentValues values) { 82 | Log.i(TAG, "insert: " + uri); 83 | 84 | long id = -1; 85 | 86 | switch (sUriMatcher.match(uri)) { 87 | case TABLE_PACKAGE_RULE_DIR: 88 | case TABLE_PACKAGE_RULE_ITEM: 89 | id = LitePal.getDatabase().insert(PackageRule.Table.NAME, null, values); 90 | return Uri.parse("content://" + AUTHORITY + "/" + PATH_PACKAGE_RULE + "/" + values.get(PackageRule.Column.PACKAGE)); 91 | case TABLE_ACTION_RECORD_DIR: 92 | case TABLE_ACTION_RECORD_ITEM: 93 | id = LitePal.getDatabase().insert(ActionRecord.Table.NAME, null, values); 94 | showActionMessage(values); 95 | return Uri.parse("content://" + AUTHORITY + "/" + PATH_ACTION_RECORD + "/" + values.get(ActionRecord.Column.PACKAGE)); 96 | } 97 | return null; 98 | } 99 | 100 | private void showActionMessage(ContentValues values) { 101 | StringBuilder sb = new StringBuilder(64); 102 | switch (values.getAsInteger(ActionRecord.Column.ACTION)) { 103 | case ActionRecord.ACTION_DENY: sb.append("已拒绝"); break; 104 | case ActionRecord.ACTION_GRANT: sb.append("已允许"); break; 105 | default: sb.append("未知操作"); break; 106 | } 107 | sb.append(values.getAsString(ActionRecord.Column.APP_NAME)) 108 | .append("修改剪贴板为:"); 109 | String text = values.getAsString(ActionRecord.Column.TEXT); 110 | if (text.length() > 16) { 111 | sb.append(text, 0, 16).append("..."); 112 | } 113 | else { 114 | sb.append(text); 115 | } 116 | ThisApp.mH.post(() -> ToastUtils.getInstance(getContext()).show(sb)); 117 | } 118 | 119 | @Override 120 | public Cursor query(@NonNull Uri uri, String[] projection, String selection, 121 | String[] selectionArgs, String sortOrder) { 122 | Log.i(TAG, "query: " + uri); 123 | 124 | switch (sUriMatcher.match(uri)) { 125 | case TABLE_PACKAGE_RULE_DIR: 126 | return LitePal.getDatabase().query(PackageRule.Table.NAME, projection, 127 | selection, selectionArgs, null, null, sortOrder); 128 | case TABLE_PACKAGE_RULE_ITEM: 129 | return LitePal.getDatabase().query(PackageRule.Table.NAME, projection, 130 | PackageRule.Column.PACKAGE + "= ?", 131 | new String[]{uri.getLastPathSegment()}, 132 | null, null, sortOrder); 133 | case TABLE_ACTION_RECORD_DIR: 134 | return LitePal.getDatabase().query(ActionRecord.Table.NAME, projection, 135 | selection, selectionArgs, null, null, sortOrder); 136 | case TABLE_ACTION_RECORD_ITEM: 137 | return LitePal.getDatabase().query(ActionRecord.Table.NAME, projection, 138 | ActionRecord.Column.PACKAGE + "= ?", 139 | new String[]{uri.getLastPathSegment()}, 140 | null, null, sortOrder); 141 | } 142 | return null; 143 | } 144 | 145 | @Override 146 | public int update(@NonNull Uri uri, ContentValues values, String selection, 147 | String[] selectionArgs) { 148 | Log.i(TAG, "update: " + uri); 149 | switch (sUriMatcher.match(uri)) { 150 | case TABLE_PACKAGE_RULE_DIR: 151 | return LitePal.getDatabase().update(PackageRule.Table.NAME, values, selection, selectionArgs); 152 | case TABLE_PACKAGE_RULE_ITEM: 153 | return LitePal.getDatabase().update(PackageRule.Table.NAME, values, 154 | PackageRule.Column.PACKAGE + "= ?", 155 | new String[]{uri.getLastPathSegment()}); 156 | case TABLE_ACTION_RECORD_DIR: 157 | return LitePal.getDatabase().update(ActionRecord.Table.NAME, values, selection, selectionArgs); 158 | case TABLE_ACTION_RECORD_ITEM: 159 | return LitePal.getDatabase().update(ActionRecord.Table.NAME, values, 160 | ActionRecord.Column.PACKAGE + "= ?", 161 | new String[] {uri.getLastPathSegment()}); 162 | } 163 | return 0; 164 | } 165 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/receiver/BootReceiver.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.receiver; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.util.Log; 7 | 8 | import androidx.core.content.ContextCompat; 9 | 10 | import cn.nlifew.clipmgr.service.AliveService; 11 | 12 | public class BootReceiver extends BroadcastReceiver { 13 | private static final String TAG = "BootReceiver"; 14 | 15 | @Override 16 | public void onReceive(Context context, Intent intent) { 17 | final String action = intent.getAction(); 18 | Log.i(TAG, "onReceive: " + action); 19 | 20 | Intent it = new Intent(context, AliveService.class); 21 | ContextCompat.startForegroundService(context, it); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/service/AliveService.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.service; 2 | 3 | import android.annotation.TargetApi; 4 | import android.app.Notification; 5 | import android.app.NotificationChannel; 6 | import android.app.NotificationManager; 7 | import android.app.PendingIntent; 8 | import android.app.Service; 9 | import android.content.Context; 10 | import android.content.Intent; 11 | import android.content.res.Resources; 12 | import android.os.Build; 13 | import android.os.IBinder; 14 | 15 | import androidx.core.app.NotificationCompat; 16 | 17 | import cn.nlifew.clipmgr.R; 18 | import cn.nlifew.clipmgr.ui.about.AboutActivity; 19 | 20 | public class AliveService extends Service { 21 | private static final String TAG = "AliveService"; 22 | 23 | private static final int NOTIFICATION_ID = 10; 24 | private static final String CHANNEL_ID = "package_rules_provider"; 25 | 26 | @Override 27 | public IBinder onBind(Intent intent) { 28 | throw new UnsupportedOperationException("Not yet implemented"); 29 | } 30 | 31 | 32 | @Override 33 | public void onCreate() { 34 | Notification.Builder builder = makeBuilder(this); 35 | Notification notification = builder.build(); 36 | startForeground(NOTIFICATION_ID, notification); 37 | } 38 | 39 | @Override 40 | public void onDestroy() { 41 | stopForeground(true); 42 | } 43 | 44 | private Notification.Builder makeBuilder(Context context) { 45 | NotificationManager nm = (NotificationManager) context 46 | .getSystemService(NOTIFICATION_SERVICE); 47 | if (nm == null) { 48 | return null; 49 | } 50 | 51 | final Notification.Builder builder; 52 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 53 | NotificationChannel channel = makeChannel(context); 54 | nm.createNotificationChannel(channel); 55 | builder = new Notification.Builder(context, CHANNEL_ID); 56 | } 57 | else { 58 | builder = new Notification.Builder(context); 59 | } 60 | 61 | Resources res = context.getResources(); 62 | 63 | builder.setContentTitle(res.getText(R.string.alive_notification_title)); 64 | builder.setContentText(res.getText(R.string.alive_notification_content)); 65 | builder.setWhen(System.currentTimeMillis()); 66 | builder.setSmallIcon(R.mipmap.ic_launcher_round); 67 | 68 | PendingIntent pi = PendingIntent.getActivity(this, 69 | 10, new Intent(this, AboutActivity.class), 70 | PendingIntent.FLAG_UPDATE_CURRENT); 71 | builder.setContentIntent(pi); 72 | return builder; 73 | } 74 | 75 | @TargetApi(Build.VERSION_CODES.O) 76 | private static NotificationChannel makeChannel(Context context) { 77 | NotificationChannel channel = new NotificationChannel(CHANNEL_ID, 78 | context.getString(R.string.alive_channel_name), 79 | NotificationManager.IMPORTANCE_HIGH); 80 | channel.enableLights(false); 81 | channel.enableVibration(false); 82 | channel.setDescription(context.getString(R.string.alive_channel_description)); 83 | return channel; 84 | } 85 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/settings/Settings.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.settings; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | import java.util.Map; 7 | 8 | public final class Settings { 9 | 10 | private static Settings sInstance; 11 | 12 | public static Settings getInstance(Context c) { 13 | if (sInstance == null) { 14 | synchronized (Settings.class) { 15 | if (sInstance == null) { 16 | sInstance = new Settings(c); 17 | } 18 | } 19 | } 20 | return sInstance; 21 | } 22 | 23 | private Settings(Context c) { 24 | mContext = c.getApplicationContext(); 25 | mPref = mContext.getSharedPreferences( 26 | PREF_NAME, Context.MODE_PRIVATE); 27 | } 28 | 29 | private final Context mContext; 30 | private final SharedPreferences mPref; 31 | 32 | public static final String PREF_NAME = "settings"; 33 | public static final String KEY_SHOW_SYSTEM_APP = "show_system_app"; 34 | public static final String KEY_VERSION_CODE = "version_code"; 35 | 36 | public boolean isShowSystemApp() { 37 | return mPref.getBoolean(KEY_SHOW_SYSTEM_APP, false); 38 | } 39 | 40 | public void setShowSystemApp(boolean show) { 41 | mPref.edit().putBoolean(KEY_SHOW_SYSTEM_APP, show).apply(); 42 | } 43 | 44 | public int getVersionCode() { 45 | // 这个 key 在 versionCode 为 5 的时候添加 46 | return mPref.getInt(KEY_VERSION_CODE, 5); 47 | } 48 | 49 | public void setVersionCode(int code) { 50 | mPref.edit().putInt(KEY_VERSION_CODE, code).apply(); 51 | } 52 | 53 | public Map getAll() { return mPref.getAll(); } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/ui/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.ui; 2 | 3 | 4 | import androidx.appcompat.app.AppCompatActivity; 5 | 6 | public abstract class BaseActivity extends AppCompatActivity { 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/ui/EmptyActivity.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.ui; 2 | 3 | import android.content.ClipData; 4 | import android.content.ClipboardManager; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.net.Uri; 8 | import android.os.Bundle; 9 | import android.os.IBinder; 10 | import android.os.ServiceManager; 11 | import android.util.Log; 12 | 13 | import java.lang.reflect.Field; 14 | import java.util.Map; 15 | 16 | import cn.nlifew.clipmgr.BuildConfig; 17 | import cn.nlifew.clipmgr.R; 18 | import cn.nlifew.clipmgr.ui.about.AboutActivity; 19 | import cn.nlifew.clipmgr.ui.main.MainActivity; 20 | import cn.nlifew.clipmgr.util.ToastUtils; 21 | 22 | public class EmptyActivity extends BaseActivity { 23 | private static final String TAG = "EmptyActivity"; 24 | 25 | @Override 26 | protected void onCreate(Bundle savedInstanceState) { 27 | super.onCreate(savedInstanceState); 28 | setContentView(R.layout.activity_empty); 29 | 30 | findViewById(R.id.activity_main_btn1) 31 | .setOnClickListener(v -> { 32 | Intent intent = new Intent(this, AboutActivity.class); 33 | startActivity(intent); 34 | }); 35 | findViewById(R.id.activity_main_btn2) 36 | .setOnClickListener(v -> { 37 | Intent intent = new Intent(this, MainActivity.class); 38 | startActivity(intent); 39 | }); 40 | } 41 | 42 | static { 43 | try { 44 | install(); 45 | } catch (Throwable e) { 46 | Log.e(TAG, "onCreate: ", e); 47 | } 48 | } 49 | 50 | private static void install() throws Throwable { 51 | if (true) { 52 | return; 53 | } 54 | 55 | IBinder bridge = ServiceManager.getService("clipmgr_bridge"); 56 | if (bridge == null) { 57 | Log.e(TAG, "install: null clipmgr_bridge service"); 58 | return; 59 | } 60 | 61 | Field sCacheField = ServiceManager.class.getDeclaredField("sCache"); 62 | sCacheField.setAccessible(true); 63 | 64 | @SuppressWarnings("unchecked") 65 | Map sCache = (Map) 66 | sCacheField.get(null); 67 | 68 | sCache.put(Context.CLIPBOARD_SERVICE, bridge); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/ui/about/AboutActivity.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.ui.about; 2 | 3 | 4 | import android.os.Bundle; 5 | import android.view.MenuItem; 6 | 7 | import androidx.annotation.NonNull; 8 | import androidx.annotation.Nullable; 9 | import androidx.appcompat.app.ActionBar; 10 | import androidx.appcompat.widget.Toolbar; 11 | import androidx.lifecycle.ViewModelProvider; 12 | import androidx.recyclerview.widget.RecyclerView; 13 | 14 | import java.util.List; 15 | 16 | import cn.nlifew.clipmgr.R; 17 | import cn.nlifew.clipmgr.ui.BaseActivity; 18 | import cn.nlifew.clipmgr.util.ToastUtils; 19 | 20 | public class AboutActivity extends BaseActivity { 21 | private static final String TAG = "AboutActivity"; 22 | 23 | @Override 24 | protected void onCreate(@Nullable Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | setContentView(R.layout.activity_about); 27 | 28 | Toolbar toolbar = findViewById(R.id.toolbar); 29 | setSupportActionBar(toolbar); 30 | 31 | ActionBar actionBar = getSupportActionBar(); 32 | if (actionBar != null) { 33 | actionBar.setDisplayHomeAsUpEnabled(true); 34 | actionBar.setDisplayShowTitleEnabled(true); 35 | } 36 | 37 | setTitle(R.string.about_title); 38 | 39 | mRecyclerAdapter = new RecyclerAdapterImpl(this); 40 | RecyclerView view = findViewById(R.id.activity_about_recycler); 41 | view.setAdapter(mRecyclerAdapter); 42 | 43 | mViewModel = new ViewModelProvider(this).get(QASModel.class); 44 | mViewModel.error().observe(this, this::onQASErrChanged); 45 | mViewModel.list().observe(this, this::onQASListChanged); 46 | } 47 | 48 | private QASModel mViewModel; 49 | private RecyclerAdapterImpl mRecyclerAdapter; 50 | 51 | @Override 52 | protected void onResume() { 53 | super.onResume(); 54 | 55 | List list = mViewModel.list().getValue(); 56 | if (list == null || list.size() == 0) { 57 | mViewModel.loadData("QAS.xml"); 58 | } 59 | } 60 | 61 | private void onQASListChanged(List list) { 62 | if (list == null) { 63 | return; 64 | } 65 | mRecyclerAdapter.updateDataSet(list); 66 | } 67 | 68 | private void onQASErrChanged(Exception e) { 69 | if (e == null) { 70 | return; 71 | } 72 | ToastUtils.getInstance(this).show(e.toString()); 73 | } 74 | 75 | 76 | @Override 77 | public boolean onOptionsItemSelected(@NonNull MenuItem item) { 78 | final int id = item.getItemId(); 79 | if (id == android.R.id.home) { 80 | finish(); 81 | return true; 82 | } 83 | return super.onOptionsItemSelected(item); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/ui/about/LoadQASTask.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.ui.about; 2 | 3 | import android.content.res.AssetManager; 4 | import android.os.AsyncTask; 5 | import android.util.Log; 6 | import android.util.Xml; 7 | 8 | import androidx.lifecycle.MutableLiveData; 9 | 10 | import org.xmlpull.v1.XmlPullParser; 11 | import org.xmlpull.v1.XmlPullParserException; 12 | 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.nio.charset.StandardCharsets; 16 | import java.util.ArrayList; 17 | import java.util.Arrays; 18 | import java.util.List; 19 | 20 | final class LoadQASTask extends AsyncTask> { 21 | private static final String TAG = "LoadQASTask"; 22 | 23 | LoadQASTask(AssetManager am, MutableLiveData err, MutableLiveData> result) { 24 | mAm = am; 25 | mError = err; 26 | mResult = result; 27 | } 28 | 29 | private final AssetManager mAm; 30 | private final MutableLiveData mError; 31 | private final MutableLiveData> mResult; 32 | 33 | 34 | @Override 35 | protected List doInBackground(String... strings) { 36 | List list = new ArrayList<>(); 37 | 38 | try (InputStream is = mAm.open(strings[0])) { 39 | XmlPullParser xml = Xml.newPullParser(); 40 | xml.setInput(is, "UTF-8"); 41 | 42 | for (int ev = xml.getEventType(); ev != XmlPullParser.END_DOCUMENT; ev = xml.next()) { 43 | if (ev != XmlPullParser.START_TAG || ! "item".equals(xml.getName())) { 44 | continue; 45 | } 46 | final String q = xml.getAttributeValue(null, "question"); 47 | final String a = xml.nextText().trim(); 48 | 49 | list.add(new QAS(q, a)); 50 | } 51 | mResult.postValue(list); 52 | } catch (IOException| XmlPullParserException e) { 53 | mError.postValue(e); 54 | Log.e(TAG, "doInBackground: " + Arrays.toString(strings), e); 55 | } 56 | return null; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/ui/about/QAS.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.ui.about; 2 | 3 | public class QAS { 4 | public String question; 5 | public String answer; 6 | 7 | public QAS() { 8 | } 9 | 10 | public QAS(String q, String a) { 11 | question = q; 12 | answer = a; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/ui/about/QASModel.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.ui.about; 2 | 3 | import android.app.Application; 4 | import android.content.res.AssetManager; 5 | 6 | import androidx.annotation.NonNull; 7 | import androidx.lifecycle.AndroidViewModel; 8 | import androidx.lifecycle.LiveData; 9 | import androidx.lifecycle.MutableLiveData; 10 | import androidx.lifecycle.ViewModel; 11 | 12 | import java.util.List; 13 | 14 | public class QASModel extends AndroidViewModel { 15 | 16 | public QASModel(@NonNull Application application) { 17 | super(application); 18 | } 19 | 20 | private final MutableLiveData mError = new MutableLiveData<>(null); 21 | private final MutableLiveData> mList = new MutableLiveData<>(null); 22 | 23 | LiveData error() { return mError; } 24 | LiveData> list() { return mList; } 25 | 26 | void loadData(String name) { 27 | AssetManager am = getApplication().getAssets(); 28 | new LoadQASTask(am, mError, mList).execute(name); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/ui/about/RecyclerAdapterImpl.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.ui.about; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.TextView; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.recyclerview.widget.RecyclerView; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | import cn.nlifew.clipmgr.R; 16 | 17 | final class RecyclerAdapterImpl extends RecyclerView.Adapter { 18 | 19 | RecyclerAdapterImpl(Context context) { 20 | mContext = context; 21 | } 22 | 23 | private final Context mContext; 24 | private final List mDataSet = new ArrayList<>(8); 25 | 26 | void updateDataSet(List list) { 27 | mDataSet.clear(); // todo: notifyItemRangeRemoved() 28 | mDataSet.addAll(list); 29 | notifyDataSetChanged(); 30 | } 31 | 32 | @Override 33 | public int getItemCount() { 34 | return mDataSet.size(); 35 | } 36 | 37 | @NonNull 38 | @Override 39 | public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 40 | View view = LayoutInflater.from(mContext).inflate( 41 | R.layout.activity_about_item, parent, false 42 | ); 43 | return new Holder(view); 44 | } 45 | 46 | @Override 47 | public void onBindViewHolder(@NonNull Holder holder, int position) { 48 | final QAS qas = mDataSet.get(position); 49 | 50 | holder.mQuestionView.setText(qas.question); 51 | holder.mAnswerView.setText(qas.answer); 52 | } 53 | 54 | static final class Holder extends RecyclerView.ViewHolder { 55 | 56 | Holder(@NonNull View itemView) { 57 | super(itemView); 58 | mQuestionView = itemView.findViewById(R.id.activity_about_item_q); 59 | mAnswerView = itemView.findViewById(R.id.activity_about_item_a); 60 | } 61 | 62 | final TextView mQuestionView; 63 | final TextView mAnswerView; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/ui/main/MainActivity.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.ui.main; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Activity; 5 | import android.content.DialogInterface; 6 | import android.content.Intent; 7 | import android.content.pm.PackageManager; 8 | import android.database.Cursor; 9 | import android.net.Uri; 10 | import android.os.Bundle; 11 | import android.util.Log; 12 | import android.view.Menu; 13 | import android.view.MenuItem; 14 | 15 | import androidx.annotation.Nullable; 16 | import androidx.appcompat.app.AlertDialog; 17 | import androidx.appcompat.widget.SearchView; 18 | import androidx.appcompat.widget.Toolbar; 19 | import androidx.lifecycle.ViewModelProvider; 20 | import androidx.viewpager.widget.ViewPager; 21 | 22 | import com.google.android.material.tabs.TabLayout; 23 | 24 | import cn.nlifew.clipmgr.R; 25 | import cn.nlifew.clipmgr.service.AliveService; 26 | import cn.nlifew.clipmgr.settings.Settings; 27 | import cn.nlifew.clipmgr.ui.BaseActivity; 28 | import cn.nlifew.clipmgr.ui.about.AboutActivity; 29 | 30 | public class MainActivity extends BaseActivity implements 31 | SearchView.OnQueryTextListener, 32 | SearchView.OnCloseListener { 33 | private static final String TAG = "MainActivity"; 34 | 35 | @Override 36 | protected void onCreate(@Nullable Bundle savedInstanceState) { 37 | super.onCreate(savedInstanceState); 38 | setContentView(R.layout.activity_main); 39 | 40 | Toolbar toolbar = findViewById(R.id.toolbar); 41 | setSupportActionBar(toolbar); 42 | 43 | PagerAdapterImpl adapter = new PagerAdapterImpl(this); 44 | TabLayout tabLayout = findViewById(R.id.tabLayout); 45 | ViewPager pager = findViewById(R.id.pager); 46 | pager.setAdapter(adapter); 47 | tabLayout.setupWithViewPager(pager); 48 | 49 | mViewModel = new ViewModelProvider(this).get(MainViewModel.class); 50 | 51 | Intent intent = new Intent(this, AliveService.class); 52 | startService(intent); 53 | } 54 | 55 | private MainViewModel mViewModel; 56 | 57 | 58 | @Override 59 | public boolean onCreateOptionsMenu(Menu menu) { 60 | getMenuInflater().inflate(R.menu.options_main, menu); 61 | 62 | SearchView searchView = (SearchView) menu 63 | .findItem(R.id.options_search) 64 | .getActionView(); 65 | 66 | searchView.setOnQueryTextListener(this); 67 | searchView.setOnCloseListener(this); 68 | 69 | return true; 70 | } 71 | 72 | @Override 73 | public boolean onPrepareOptionsMenu(Menu menu) { 74 | Settings settings = Settings.getInstance(this); 75 | 76 | menu.findItem(R.id.options_show_system) 77 | .setChecked(settings.isShowSystemApp()); 78 | 79 | return true; 80 | } 81 | 82 | @Override 83 | @SuppressLint("NonConstantResourceId") 84 | public boolean onOptionsItemSelected(MenuItem item) { 85 | switch (item.getItemId()) { 86 | case R.id.options_clear_history: 87 | mViewModel.clearActionRecordList(); 88 | return true; 89 | case R.id.options_show_system: 90 | Settings.getInstance(this) 91 | .setShowSystemApp(! item.isChecked()); 92 | mViewModel.clearAll(); 93 | return true; 94 | case R.id.options_about: 95 | showAboutDialog(); 96 | return true; 97 | case R.id.options_qas: 98 | startActivity(new Intent(this, AboutActivity.class)); 99 | return true; 100 | } 101 | return super.onOptionsItemSelected(item); 102 | } 103 | 104 | private void showAboutDialog() { 105 | new AlertDialog.Builder(this) 106 | .setTitle(R.string.app_name) 107 | .setMessage(R.string.xposed_module_description) 108 | .setPositiveButton("确定", null) 109 | .show(); 110 | } 111 | 112 | /* SearchView */ 113 | 114 | @Override 115 | public boolean onClose() { 116 | mViewModel.setFilterName(null); 117 | mViewModel.clearAll(); 118 | return false; 119 | } 120 | 121 | @Override 122 | public boolean onQueryTextSubmit(String query) { 123 | mViewModel.setFilterName(query); 124 | mViewModel.clearAll(); 125 | return false; 126 | } 127 | 128 | @Override 129 | public boolean onQueryTextChange(String newText) { 130 | return false; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/ui/main/MainFragment.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.ui.main; 2 | 3 | import android.os.Bundle; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | 8 | import androidx.annotation.NonNull; 9 | import androidx.annotation.Nullable; 10 | import androidx.lifecycle.ViewModelProvider; 11 | import androidx.recyclerview.widget.LinearLayoutManager; 12 | import androidx.recyclerview.widget.RecyclerView; 13 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; 14 | 15 | import cn.nlifew.clipmgr.R; 16 | import cn.nlifew.clipmgr.fragment.BaseFragment; 17 | 18 | public abstract class MainFragment extends BaseFragment implements 19 | SwipeRefreshLayout.OnRefreshListener{ 20 | 21 | @Override 22 | public void onCreate(@Nullable Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | 25 | mViewModel = new ViewModelProvider(getActivity()).get(MainViewModel.class); 26 | } 27 | 28 | @Nullable 29 | @Override 30 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 31 | View view = inflater.inflate(R.layout.fragment_main, container, false); 32 | mSwipeRefreshLayout = view.findViewById(R.id.refresh); 33 | mSwipeRefreshLayout.setOnRefreshListener(this); 34 | 35 | mRecyclerView = view.findViewById(R.id.recycler); 36 | mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); 37 | return view; 38 | } 39 | 40 | protected SwipeRefreshLayout mSwipeRefreshLayout; 41 | protected RecyclerView mRecyclerView; 42 | protected MainViewModel mViewModel; 43 | 44 | @Override 45 | public void onRefresh() { 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/ui/main/MainViewModel.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.ui.main; 2 | 3 | import android.content.pm.ApplicationInfo; 4 | import android.content.pm.PackageInfo; 5 | import android.content.pm.PackageManager; 6 | import android.os.AsyncTask; 7 | import android.util.Log; 8 | 9 | import androidx.lifecycle.LiveData; 10 | import androidx.lifecycle.MutableLiveData; 11 | import androidx.lifecycle.ViewModel; 12 | 13 | import org.litepal.LitePal; 14 | 15 | import java.util.ArrayList; 16 | import java.util.HashMap; 17 | import java.util.List; 18 | import java.util.Locale; 19 | import java.util.Map; 20 | 21 | import cn.nlifew.clipmgr.BuildConfig; 22 | import cn.nlifew.clipmgr.app.ThisApp; 23 | import cn.nlifew.clipmgr.bean.ActionRecord; 24 | import cn.nlifew.clipmgr.bean.PackageRule; 25 | import cn.nlifew.clipmgr.settings.Settings; 26 | import cn.nlifew.clipmgr.ui.main.record.RecordWrapper; 27 | import cn.nlifew.clipmgr.ui.main.rule.RuleWrapper; 28 | 29 | public class MainViewModel extends ViewModel { 30 | private static final String TAG = "MainViewModel"; 31 | 32 | public MainViewModel() { 33 | 34 | } 35 | 36 | private final MutableLiveData mErrMsg = new MutableLiveData<>(null); 37 | private final MutableLiveData> mPackageRuleList = new MutableLiveData<>(null); 38 | private final MutableLiveData> mActionRecordList = new MutableLiveData<>(null); 39 | 40 | private String mFilterName; 41 | 42 | void setFilterName(String appName) { 43 | if (appName == null) { 44 | mFilterName = null; 45 | } else { 46 | mFilterName = appName.toLowerCase(Locale.getDefault()); 47 | } 48 | } 49 | 50 | void clearAll() { 51 | mPackageRuleList.postValue(null); 52 | mActionRecordList.postValue(null); 53 | } 54 | 55 | void clearActionRecordList() { 56 | LitePal.deleteAll(ActionRecord.class); 57 | mActionRecordList.postValue(new ArrayList<>(0)); 58 | } 59 | 60 | public void loadPackageRuleList() { 61 | new LoadPackageRuleTask().execute(); 62 | } 63 | 64 | public void loadActionRecordList() { 65 | new LoadActionRecordTask().execute(); 66 | } 67 | 68 | 69 | public LiveData getErrMsg() { return mErrMsg; } 70 | 71 | public LiveData> getPackageRuleList() { return mPackageRuleList; } 72 | 73 | public LiveData> getActionRecordList() { return mActionRecordList; } 74 | 75 | 76 | private final class LoadPackageRuleTask extends AsyncTask { 77 | 78 | @Override 79 | protected Void doInBackground(Void... voids) { 80 | Log.d(TAG, "doInBackground: start"); 81 | 82 | PackageManager pm = ThisApp 83 | .currentApplication 84 | .getPackageManager(); 85 | 86 | final String filterName = mFilterName; 87 | boolean showSystemApp = Settings 88 | .getInstance(ThisApp.currentApplication) 89 | .isShowSystemApp(); 90 | 91 | try { 92 | List ruleList = LitePal.findAll(PackageRule.class); 93 | Map ruleMap = new HashMap<>(ruleList.size()); 94 | for (PackageRule rule : ruleList) { 95 | ruleMap.put(rule.getPkg(), rule); 96 | } 97 | 98 | List infoList = pm.getInstalledPackages(0); 99 | List wrapperList = new ArrayList<>(infoList.size()); 100 | 101 | for (PackageInfo info : infoList) { 102 | 103 | // 过滤掉系统应用 104 | if (! showSystemApp && (info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { 105 | continue; 106 | } 107 | // 过滤掉我自己 108 | if (BuildConfig.APPLICATION_ID.equals(info.packageName)) { 109 | continue; 110 | } 111 | // 过滤掉不含关键字的 112 | String appName = info.applicationInfo.loadLabel(pm).toString(); 113 | if (filterName != null && ! appName.contains(filterName)) { 114 | continue; 115 | } 116 | 117 | RuleWrapper wrapper = new RuleWrapper(); 118 | wrapper.appName = appName; 119 | wrapper.icon = info.applicationInfo.loadIcon(pm); 120 | wrapper.rawRule = ruleMap.get(info.packageName); 121 | if (wrapper.rawRule == null) { 122 | wrapper.rawRule = new PackageRule(info.packageName, PackageRule.RULE_REQUEST); 123 | } 124 | 125 | wrapperList.add(wrapper); 126 | } 127 | 128 | mPackageRuleList.postValue(wrapperList); 129 | } catch (Throwable t) { 130 | Log.e(TAG, "doInBackground: ", t); 131 | mErrMsg.postValue(t.toString()); 132 | } 133 | return null; 134 | } 135 | } 136 | 137 | private final class LoadActionRecordTask extends AsyncTask { 138 | @Override 139 | protected Void doInBackground(Void... voids) { 140 | Log.d(TAG, "doInBackground: start"); 141 | PackageManager pm = ThisApp 142 | .currentApplication 143 | .getPackageManager(); 144 | 145 | final String filterName = mFilterName; 146 | boolean showSystemApp = Settings 147 | .getInstance(ThisApp.currentApplication) 148 | .isShowSystemApp(); 149 | 150 | try { 151 | List recordList = LitePal 152 | .order(ActionRecord.Column.TIME + " desc") 153 | .find(ActionRecord.class); 154 | 155 | List wrapperList = new ArrayList<>(recordList.size()); 156 | 157 | for (ActionRecord record : recordList) { 158 | 159 | // 检查当前 app 是否还存在 160 | ApplicationInfo info; 161 | try { 162 | info = pm.getApplicationInfo(record.getPkg(), 0); 163 | } catch (PackageManager.NameNotFoundException e) { 164 | Log.e(TAG, "doInBackground: missing package " + record.getPkg(), e); 165 | record.delete(); 166 | continue; 167 | } 168 | 169 | // 过滤掉系统应用 170 | if (! showSystemApp && (info.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { 171 | continue; 172 | } 173 | // 过滤掉我自己 174 | if (BuildConfig.APPLICATION_ID.equals(info.packageName)) { 175 | continue; 176 | } 177 | // 过滤掉不含关键词的 178 | String appName = info.loadLabel(pm).toString(); 179 | if (filterName != null && ! appName 180 | .toLowerCase(Locale.getDefault()) 181 | .contains(filterName)) { 182 | continue; 183 | } 184 | 185 | RecordWrapper wrapper = new RecordWrapper(); 186 | wrapper.icon = info.loadIcon(pm); 187 | wrapper.rawRecord = record; 188 | wrapperList.add(wrapper); 189 | } 190 | mActionRecordList.postValue(wrapperList); 191 | } catch (Throwable t) { 192 | Log.e(TAG, "doInBackground: ", t); 193 | mErrMsg.postValue(t.toString()); 194 | } 195 | return null; 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/ui/main/PagerAdapterImpl.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.ui.main; 2 | 3 | import android.app.Activity; 4 | import android.util.Log; 5 | import android.view.ViewGroup; 6 | 7 | import androidx.annotation.NonNull; 8 | import androidx.annotation.Nullable; 9 | import androidx.fragment.app.Fragment; 10 | import androidx.fragment.app.FragmentActivity; 11 | import androidx.fragment.app.FragmentManager; 12 | import androidx.fragment.app.FragmentPagerAdapter; 13 | 14 | import java.util.List; 15 | 16 | import cn.nlifew.clipmgr.R; 17 | import cn.nlifew.clipmgr.ui.main.record.RecordFragment; 18 | import cn.nlifew.clipmgr.ui.main.rule.RuleFragment; 19 | 20 | class PagerAdapterImpl extends FragmentPagerAdapter { 21 | private static final String TAG = "PagerAdapterImpl"; 22 | 23 | PagerAdapterImpl(FragmentActivity activity) { 24 | super(activity.getSupportFragmentManager(), BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); 25 | mActivity = activity; 26 | } 27 | 28 | private final Activity mActivity; 29 | 30 | @Override 31 | public int getCount() { 32 | return 2; 33 | } 34 | 35 | @NonNull 36 | @Override 37 | public Fragment getItem(int position) { 38 | switch (position) { 39 | case 0: return new RuleFragment(); 40 | case 1: return new RecordFragment(); 41 | } 42 | throw new ArrayIndexOutOfBoundsException(position); 43 | } 44 | 45 | @Nullable 46 | @Override 47 | public CharSequence getPageTitle(int position) { 48 | switch (position) { 49 | case 0: return mActivity.getString(R.string.title_app); 50 | case 1: return mActivity.getString(R.string.title_history); 51 | } 52 | throw new ArrayIndexOutOfBoundsException(position); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/ui/main/record/RecordFragment.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.ui.main.record; 2 | 3 | 4 | import android.os.Bundle; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.annotation.Nullable; 11 | import androidx.lifecycle.LifecycleOwner; 12 | 13 | import java.util.List; 14 | 15 | import cn.nlifew.clipmgr.ui.main.MainFragment; 16 | import cn.nlifew.clipmgr.util.ToastUtils; 17 | 18 | public class RecordFragment extends MainFragment { 19 | private static final String TAG = "RecordFragment"; 20 | 21 | @Nullable 22 | @Override 23 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 24 | View view = super.onCreateView(inflater, container, savedInstanceState); 25 | 26 | mRecyclerAdapter = new RecyclerAdapterImpl(this); 27 | mRecyclerView.setAdapter(mRecyclerAdapter); 28 | 29 | LifecycleOwner owner = getViewLifecycleOwner(); 30 | mViewModel.getErrMsg().observe(owner, this::onErrMsgChanged); 31 | mViewModel.getActionRecordList().observe(owner, this::onActionRecordListChanged); 32 | 33 | return view; 34 | } 35 | 36 | private RecyclerAdapterImpl mRecyclerAdapter; 37 | 38 | @Override 39 | public void onRefresh() { 40 | mSwipeRefreshLayout.setRefreshing(true); 41 | mViewModel.loadActionRecordList(); 42 | } 43 | 44 | private void onErrMsgChanged(String msg) { 45 | if (msg != null) { 46 | ToastUtils.getInstance(getContext()).show(msg); 47 | mSwipeRefreshLayout.setRefreshing(false); 48 | } 49 | } 50 | 51 | private void onActionRecordListChanged(List list) { 52 | if (list == null) { 53 | onRefresh(); 54 | return; 55 | } 56 | mSwipeRefreshLayout.setRefreshing(false); 57 | mRecyclerAdapter.updateDataSet(list); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/ui/main/record/RecordWrapper.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.ui.main.record; 2 | 3 | import android.graphics.drawable.Drawable; 4 | 5 | import cn.nlifew.clipmgr.bean.ActionRecord; 6 | 7 | public class RecordWrapper { 8 | public Drawable icon; 9 | public ActionRecord rawRecord; 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/ui/main/record/RecyclerAdapterImpl.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.ui.main.record; 2 | 3 | import android.content.ClipData; 4 | import android.content.Context; 5 | import android.content.DialogInterface; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.ImageView; 10 | import android.widget.TextView; 11 | 12 | import androidx.annotation.NonNull; 13 | import androidx.appcompat.app.AlertDialog; 14 | import androidx.fragment.app.Fragment; 15 | import androidx.recyclerview.widget.RecyclerView; 16 | 17 | import java.text.SimpleDateFormat; 18 | import java.util.ArrayList; 19 | import java.util.Date; 20 | import java.util.List; 21 | import java.util.Locale; 22 | 23 | import cn.nlifew.clipmgr.R; 24 | import cn.nlifew.clipmgr.bean.ActionRecord; 25 | import cn.nlifew.clipmgr.util.ClipUtils; 26 | import cn.nlifew.clipmgr.util.ToastUtils; 27 | 28 | class RecyclerAdapterImpl extends RecyclerView.Adapter { 29 | private static final String TAG = "RecyclerAdapterImpl"; 30 | 31 | RecyclerAdapterImpl(Fragment fragment) { 32 | mFragment = fragment; 33 | } 34 | 35 | private final Fragment mFragment; 36 | private final List mDataSet = new ArrayList<>(64); 37 | 38 | void updateDataSet(List list) { 39 | mDataSet.clear(); 40 | mDataSet.addAll(list); 41 | notifyDataSetChanged(); 42 | } 43 | 44 | @Override 45 | public int getItemCount() { 46 | return mDataSet.size(); 47 | } 48 | 49 | @NonNull 50 | @Override 51 | public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 52 | View view = LayoutInflater.from(mFragment.getContext()) 53 | .inflate(R.layout.fragment_main_item, parent, false); 54 | return new Holder(view); 55 | } 56 | 57 | @Override 58 | public void onBindViewHolder(@NonNull RecyclerView.ViewHolder h, int position) { 59 | Holder holder = (Holder) h; 60 | RecordWrapper record = mDataSet.get(position); 61 | 62 | holder.labelView.setText(record.rawRecord.getAppName()); 63 | holder.iconView.setImageDrawable(record.icon); 64 | holder.actionView.setText(action2str(record.rawRecord.getAction())); 65 | 66 | mDate.setTime(record.rawRecord.getTime()); 67 | holder.timeView.setText(mDateFormat.format(mDate)); 68 | 69 | holder.itemView.setTag(record); 70 | } 71 | 72 | private final Date mDate = new Date(); 73 | private final SimpleDateFormat mDateFormat = new SimpleDateFormat( 74 | "MM-dd HH:mm:ss", Locale.getDefault()); 75 | 76 | private final class Holder extends RecyclerView.ViewHolder implements View.OnClickListener{ 77 | Holder(@NonNull View itemView) { 78 | super(itemView); 79 | labelView = itemView.findViewById(R.id.label); 80 | iconView = itemView.findViewById(R.id.icon); 81 | timeView = itemView.findViewById(R.id.pkg); 82 | actionView = itemView.findViewById(R.id.action); 83 | 84 | itemView.setOnClickListener(this); 85 | } 86 | 87 | final ImageView iconView; 88 | final TextView labelView; 89 | final TextView timeView; 90 | final TextView actionView; 91 | 92 | @Override 93 | public void onClick(View v) { 94 | RecordWrapper record = (RecordWrapper) itemView.getTag(); 95 | 96 | DialogInterface.OnClickListener cli = (dialog, which) -> { 97 | ClipData clipData = ClipData.newPlainText( 98 | record.rawRecord.getAppName(), 99 | record.rawRecord.getText()); 100 | Context context = mFragment.getContext(); 101 | ClipUtils.setPrimaryClip(context, clipData); 102 | ToastUtils.getInstance(context).show("已复制到剪贴板"); 103 | }; 104 | 105 | new AlertDialog.Builder(mFragment.getActivity()) 106 | .setTitle("访问记录") 107 | .setPositiveButton("复制", cli) 108 | .setNeutralButton(action2str(record.rawRecord.getAction()), null) 109 | .setMessage(record.rawRecord.getText()) 110 | .show(); 111 | } 112 | } 113 | 114 | private static String action2str(int action) { 115 | switch (action) { 116 | case ActionRecord.ACTION_GRANT: 117 | return ("已允许"); 118 | case ActionRecord.ACTION_DENY: 119 | return ("已拒绝"); 120 | } 121 | return ("未知操作"); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/ui/main/rule/RecyclerAdapterImpl.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.ui.main.rule; 2 | 3 | import android.content.DialogInterface; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.ImageView; 8 | import android.widget.TextView; 9 | 10 | import androidx.annotation.NonNull; 11 | import androidx.appcompat.app.AlertDialog; 12 | import androidx.fragment.app.Fragment; 13 | import androidx.recyclerview.widget.RecyclerView; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | import cn.nlifew.clipmgr.R; 19 | import cn.nlifew.clipmgr.bean.PackageRule; 20 | 21 | class RecyclerAdapterImpl extends RecyclerView.Adapter { 22 | 23 | RecyclerAdapterImpl(Fragment fragment) { 24 | mFragment = fragment; 25 | } 26 | 27 | private final Fragment mFragment; 28 | private final List mDataSet = new ArrayList<>(64); 29 | 30 | void updateDataSet(List list) { 31 | mDataSet.clear(); 32 | mDataSet.addAll(list); 33 | notifyDataSetChanged(); 34 | } 35 | 36 | @Override 37 | public int getItemCount() { 38 | return mDataSet.size(); 39 | } 40 | 41 | @NonNull 42 | @Override 43 | public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 44 | View view = LayoutInflater.from(mFragment.getContext()) 45 | .inflate(R.layout.fragment_main_item, parent, false); 46 | return new Holder(view); 47 | } 48 | 49 | @Override 50 | public void onBindViewHolder(@NonNull RecyclerView.ViewHolder h, int position) { 51 | Holder holder = (Holder) h; 52 | RuleWrapper rule = mDataSet.get(position); 53 | 54 | holder.labelView.setText(rule.appName); 55 | holder.packageView.setText(rule.rawRule.getPkg()); 56 | holder.iconView.setImageDrawable(rule.icon); 57 | 58 | switch (rule.rawRule.getRule()) { 59 | case PackageRule.RULE_REQUEST: 60 | holder.actionView.setText("询问"); 61 | break; 62 | case PackageRule.RULE_GRANT: 63 | holder.actionView.setText("允许"); 64 | break; 65 | case PackageRule.RULE_DENY: 66 | holder.actionView.setText("拒绝"); 67 | break; 68 | default: 69 | holder.actionView.setText(""); 70 | } 71 | holder.itemView.setTag(rule); 72 | } 73 | 74 | private final class Holder extends RecyclerView.ViewHolder implements View.OnClickListener { 75 | Holder(@NonNull View itemView) { 76 | super(itemView); 77 | labelView = itemView.findViewById(R.id.label); 78 | iconView = itemView.findViewById(R.id.icon); 79 | packageView = itemView.findViewById(R.id.pkg); 80 | actionView = itemView.findViewById(R.id.action); 81 | 82 | itemView.setOnClickListener(this); 83 | } 84 | 85 | final ImageView iconView; 86 | final TextView labelView; 87 | final TextView packageView; 88 | final TextView actionView; 89 | 90 | @Override 91 | public void onClick(View v) { 92 | RuleWrapper rule = (RuleWrapper) itemView.getTag(); 93 | 94 | DialogInterface.OnClickListener cli = (dialog, which) -> { 95 | dialog.dismiss(); 96 | switch (which) { 97 | case 0: rule.rawRule.setRule(PackageRule.RULE_REQUEST); break; 98 | case 1: rule.rawRule.setRule(PackageRule.RULE_GRANT); break; 99 | case 2: rule.rawRule.setRule(PackageRule.RULE_DENY); break; 100 | } 101 | rule.rawRule.save(); 102 | notifyItemChanged(getAdapterPosition()); 103 | }; 104 | int checkedItem = 0; 105 | switch (rule.rawRule.getRule()) { 106 | case PackageRule.RULE_REQUEST: checkedItem = 0; break; 107 | case PackageRule.RULE_GRANT: checkedItem = 1; break; 108 | case PackageRule.RULE_DENY: checkedItem = 2; break; 109 | } 110 | 111 | new AlertDialog.Builder(mFragment.getActivity()) 112 | .setTitle(rule.appName) 113 | .setSingleChoiceItems(new String[] {"询问", "允许", "拒绝"}, checkedItem, cli) 114 | .show(); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/ui/main/rule/RuleFragment.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.ui.main.rule; 2 | 3 | import android.os.Bundle; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | 8 | import androidx.annotation.NonNull; 9 | import androidx.annotation.Nullable; 10 | import androidx.lifecycle.LifecycleOwner; 11 | 12 | import java.util.List; 13 | 14 | import cn.nlifew.clipmgr.ui.main.MainFragment; 15 | import cn.nlifew.clipmgr.util.ToastUtils; 16 | 17 | public class RuleFragment extends MainFragment { 18 | private static final String TAG = "RuleFragment"; 19 | 20 | 21 | @Nullable 22 | @Override 23 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 24 | View view = super.onCreateView(inflater, container, savedInstanceState); 25 | 26 | mRecyclerAdapter = new RecyclerAdapterImpl(this); 27 | mRecyclerView.setAdapter(mRecyclerAdapter); 28 | 29 | LifecycleOwner owner = getViewLifecycleOwner(); 30 | mViewModel.getErrMsg().observe(owner, this::onErrMsgChanged); 31 | mViewModel.getPackageRuleList().observe(owner, this::onPackageRuleListChanged); 32 | 33 | return view; 34 | } 35 | 36 | private RecyclerAdapterImpl mRecyclerAdapter; 37 | 38 | @Override 39 | public void onRefresh() { 40 | mSwipeRefreshLayout.setRefreshing(true); 41 | mViewModel.loadPackageRuleList(); 42 | } 43 | 44 | private void onErrMsgChanged(String msg) { 45 | if (msg != null) { 46 | ToastUtils.getInstance(getContext()).show(msg); 47 | mSwipeRefreshLayout.setRefreshing(false); 48 | } 49 | } 50 | 51 | private void onPackageRuleListChanged(List list) { 52 | if (list == null) { 53 | onRefresh(); 54 | return; 55 | } 56 | mSwipeRefreshLayout.setRefreshing(false); 57 | mRecyclerAdapter.updateDataSet(list); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/ui/main/rule/RuleWrapper.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.ui.main.rule; 2 | 3 | import android.graphics.drawable.Drawable; 4 | 5 | import cn.nlifew.clipmgr.bean.PackageRule; 6 | 7 | public class RuleWrapper { 8 | public CharSequence appName; 9 | public Drawable icon; 10 | 11 | public PackageRule rawRule; 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/ui/request/AlertDialogLayout.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.ui.request; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Activity; 5 | import android.content.Context; 6 | import android.content.res.ColorStateList; 7 | import android.graphics.Color; 8 | import android.graphics.drawable.ColorDrawable; 9 | import android.graphics.drawable.RippleDrawable; 10 | import android.text.TextUtils; 11 | import android.util.TypedValue; 12 | import android.view.ContextThemeWrapper; 13 | import android.view.Gravity; 14 | import android.view.View; 15 | import android.view.ViewGroup; 16 | import android.widget.Button; 17 | import android.widget.CheckBox; 18 | import android.widget.ImageView; 19 | import android.widget.LinearLayout; 20 | import android.widget.TextView; 21 | 22 | import androidx.annotation.DrawableRes; 23 | import androidx.annotation.StringRes; 24 | import androidx.annotation.StyleRes; 25 | 26 | import cn.nlifew.clipmgr.util.DisplayUtils; 27 | 28 | /** 29 | * 这个 View 用于锁定 RequestDialog 的样式,防止因为每个 app 不同的 theme 30 | * 而出现不同的样式。 31 | * 为什么不使用 {@link androidx.appcompat.widget.AlertDialogLayout} ? 32 | * 因为这个 View 在宿主进程中被加载,一旦使用到 .xml 等资源文件,宿主进程 33 | * 就有可能崩溃。因此这个 View 也必须遵循同样的原则,所有资源都要通过 java 代码管理 34 | */ 35 | @SuppressLint("ViewConstructor") 36 | @Deprecated 37 | public class AlertDialogLayout extends LinearLayout { 38 | private static final String TAG = "AlertDialogLayout"; 39 | 40 | private static ContextThemeWrapper makeWrapper(Activity activity) { 41 | final @StyleRes int theme = android.R.style 42 | .Theme_Material_Light_Dialog_NoActionBar_MinWidth; 43 | return new ContextThemeWrapper(activity, theme); 44 | } 45 | 46 | public AlertDialogLayout(Activity activity) { 47 | super(makeWrapper(activity)); 48 | 49 | Context context = getContext(); 50 | 51 | setOrientation(VERTICAL); 52 | setBackgroundColor(Color.WHITE); 53 | 54 | addView(makeTitleLayout(context)); 55 | addView(makeMessageLayout(context)); 56 | addView(makeRememberLayout(context)); 57 | addView(makeToolbarLayout(context)); 58 | } 59 | 60 | private LinearLayout mTitleLayout; 61 | private ImageView mIconView; 62 | private TextView mTitleView; 63 | 64 | private TextView mMessageView; 65 | 66 | LinearLayout mRememberLayout; 67 | private TextView mRememberView; 68 | CheckBox mCheckBox; 69 | 70 | private LinearLayout mToolbarLayout; 71 | TextView mPositiveView; 72 | TextView mNegativeView; 73 | 74 | private View makeTitleLayout(Context context) { 75 | mTitleLayout = new LinearLayout(context); 76 | mTitleLayout.setOrientation(HORIZONTAL); 77 | mTitleLayout.setVisibility(GONE); 78 | 79 | int DP24 = DisplayUtils.dp2px(context, 24); 80 | int DP18 = DP24 / 4 * 3; 81 | mTitleLayout.setPadding(DP24, DP18, DP24, 0); 82 | 83 | mTitleLayout.setGravity(Gravity.CENTER_VERTICAL|Gravity.START); 84 | 85 | LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 86 | ViewGroup.LayoutParams.WRAP_CONTENT); 87 | lp.bottomMargin = DisplayUtils.dp2px(context, 5); 88 | mTitleLayout.setLayoutParams(lp); 89 | 90 | /* mIconView */ 91 | 92 | mIconView = new ImageView(context); 93 | mIconView.setVisibility(GONE); 94 | 95 | int DP32 = DP24 / 3 * 4; 96 | lp = new LayoutParams(DP32, DP32); 97 | lp.setMarginEnd(DP24 / 3); 98 | mTitleLayout.addView(mIconView, lp); 99 | 100 | /* mTitleView */ 101 | 102 | mTitleView = new TextView(context); 103 | mTitleView.setVisibility(GONE); 104 | mTitleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); 105 | mTitleView.setTextColor(0xDE000000); 106 | mTitleView.setEllipsize(TextUtils.TruncateAt.END); 107 | 108 | lp = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 109 | ViewGroup.LayoutParams.WRAP_CONTENT); 110 | mTitleLayout.addView(mTitleView, lp); 111 | 112 | return mTitleLayout; 113 | } 114 | 115 | private View makeMessageLayout(Context context) { 116 | mMessageView = new TextView(context); 117 | mMessageView.setVisibility(GONE); 118 | mMessageView.setTextColor(Color.BLACK); 119 | mMessageView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); 120 | 121 | int DP24 = DisplayUtils.dp2px(context, 24); 122 | mMessageView.setPadding(DP24, 0, DP24, 0); 123 | mMessageView.setMinHeight(DP24 * 2); 124 | 125 | LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 126 | ViewGroup.LayoutParams.WRAP_CONTENT); 127 | lp.bottomMargin = DisplayUtils.dp2px(context, 5); 128 | mMessageView.setLayoutParams(lp); 129 | 130 | return mMessageView; 131 | } 132 | 133 | private View makeRememberLayout(Context context) { 134 | mRememberLayout = new LinearLayout(context); 135 | mRememberLayout.setOrientation(HORIZONTAL); 136 | mRememberLayout.setVisibility(GONE); 137 | 138 | int DP24 = DisplayUtils.dp2px(context, 20); 139 | int DP10 = DisplayUtils.dp2px(context, 10); 140 | mRememberLayout.setPadding(DP24, DP10, DP24, DP10); 141 | 142 | LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 143 | ViewGroup.LayoutParams.WRAP_CONTENT); 144 | lp.bottomMargin = DisplayUtils.dp2px(context, 5); 145 | mRememberLayout.setLayoutParams(lp); 146 | 147 | 148 | /* mCheckBox */ 149 | mCheckBox = new CheckBox(context); 150 | lp = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 151 | ViewGroup.LayoutParams.MATCH_PARENT 152 | ); 153 | lp.rightMargin = DP10; 154 | mRememberLayout.addView(mCheckBox, lp); 155 | 156 | /* mRememberView */ 157 | mRememberView = new TextView(context); 158 | mRememberView.setGravity(Gravity.CENTER_VERTICAL); 159 | mRememberView.setTextColor(0xFF737373); 160 | mRememberView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); 161 | mRememberView.setOnClickListener(v -> mCheckBox.setChecked(! mCheckBox.isChecked())); 162 | 163 | lp = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 164 | ViewGroup.LayoutParams.MATCH_PARENT); 165 | mRememberLayout.addView(mRememberView, lp); 166 | 167 | return mRememberLayout; 168 | } 169 | 170 | 171 | private Button newToolButton(Context context) { 172 | ColorStateList csl = ColorStateList.valueOf(Color.GRAY); 173 | RippleDrawable ripple = new RippleDrawable(csl, null, null); 174 | ripple.addLayer(new ColorDrawable(Color.GRAY)); 175 | ripple.setId(0, android.R.id.mask); 176 | 177 | Button btn = new Button(context); 178 | btn.setVisibility(GONE); 179 | btn.setTextColor(0xFFD81B60); 180 | btn.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); 181 | btn.setBackground(ripple); 182 | return btn; 183 | } 184 | 185 | private View makeToolbarLayout(Context context) { 186 | mToolbarLayout = new LinearLayout(context); 187 | mToolbarLayout.setOrientation(HORIZONTAL); 188 | mToolbarLayout.setVisibility(GONE); 189 | mToolbarLayout.setGravity(Gravity.END); 190 | 191 | int DP12 = DisplayUtils.dp2px(context, 12); 192 | int DP4 = DP12 / 3; 193 | mToolbarLayout.setPadding(DP12, DP4, DP12, DP4); 194 | 195 | LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 196 | ViewGroup.LayoutParams.WRAP_CONTENT); 197 | mToolbarLayout.setLayoutParams(lp); 198 | 199 | /* mNegativeView */ 200 | int DP64 = DisplayUtils.dp2px(context, 64); 201 | int DP48 = DisplayUtils.dp2px(context, 48); 202 | 203 | mNegativeView = newToolButton(context); 204 | mToolbarLayout.addView(mNegativeView, new ViewGroup.LayoutParams( 205 | DP64, DP48 206 | )); 207 | 208 | 209 | /* mPositiveView */ 210 | mPositiveView = newToolButton(context); 211 | mToolbarLayout.addView(mPositiveView, new ViewGroup.LayoutParams( 212 | DP64, DP48 213 | )); 214 | return mToolbarLayout; 215 | } 216 | 217 | 218 | public void setTitle(@StringRes int title) { 219 | mTitleLayout.setVisibility(VISIBLE); 220 | mTitleView.setVisibility(VISIBLE); 221 | mTitleView.setText(title); 222 | } 223 | 224 | public void setIcon(@DrawableRes int icon) { 225 | mTitleLayout.setVisibility(VISIBLE); 226 | mIconView.setVisibility(VISIBLE); 227 | mIconView.setImageResource(icon); 228 | } 229 | 230 | public void setMessage(CharSequence msg) { 231 | if (TextUtils.isEmpty(msg)) { 232 | mMessageView.setVisibility(GONE); 233 | } 234 | else { 235 | mMessageView.setVisibility(VISIBLE); 236 | mMessageView.setText(msg); 237 | } 238 | } 239 | 240 | public void setRemember(CharSequence text) { 241 | if (TextUtils.isEmpty(text)) { 242 | mRememberLayout.setVisibility(GONE); 243 | } 244 | else { 245 | mRememberLayout.setVisibility(VISIBLE); 246 | mRememberView.setText(text); 247 | } 248 | } 249 | 250 | public void setPositive(CharSequence text) { 251 | if (TextUtils.isEmpty(text)) { 252 | mPositiveView.setVisibility(GONE); 253 | if (mNegativeView.getVisibility() == GONE) { 254 | mToolbarLayout.setVisibility(GONE); 255 | } 256 | } 257 | else { 258 | mToolbarLayout.setVisibility(VISIBLE); 259 | mPositiveView.setVisibility(VISIBLE); 260 | mPositiveView.setText(text); 261 | } 262 | } 263 | 264 | public void setNegative(CharSequence text) { 265 | if (TextUtils.isEmpty(text)) { 266 | mNegativeView.setText(View.GONE); 267 | if (mPositiveView.getVisibility() == GONE) { 268 | mToolbarLayout.setVisibility(GONE); 269 | } 270 | } 271 | else { 272 | mToolbarLayout.setVisibility(VISIBLE); 273 | mNegativeView.setVisibility(VISIBLE); 274 | mNegativeView.setText(text); 275 | } 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/ui/request/OnRequestFinishListener.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.ui.request; 2 | 3 | import android.content.DialogInterface; 4 | 5 | import androidx.annotation.IntDef; 6 | 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | 10 | 11 | public interface OnRequestFinishListener { 12 | int RESULT_UNKNOWN = 0; 13 | int RESULT_CANCEL = 1; 14 | int RESULT_POSITIVE = 1 << 1; 15 | int RESULT_NEGATIVE = 1 << 2; 16 | int RESULT_REMEMBER = 1 << 3; 17 | 18 | @Retention(RetentionPolicy.SOURCE) 19 | @IntDef({RESULT_UNKNOWN, RESULT_CANCEL, RESULT_POSITIVE, 20 | RESULT_NEGATIVE, RESULT_REMEMBER}) 21 | @interface flag { }; 22 | 23 | void onRequestFinish(@flag int result); 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/ui/request/RequestDialog.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.ui.request; 2 | 3 | import android.app.Activity; 4 | import android.app.Dialog; 5 | import android.content.DialogInterface; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | 9 | import androidx.annotation.DrawableRes; 10 | import androidx.annotation.StringRes; 11 | 12 | @Deprecated 13 | public class RequestDialog extends Dialog { 14 | public static class Builder { 15 | 16 | public Builder(Activity activity) { 17 | mActivity = activity; 18 | mDialogView = new AlertDialogLayout(activity); 19 | } 20 | 21 | private final Activity mActivity; 22 | private final AlertDialogLayout mDialogView; 23 | 24 | private OnRequestFinishListener mCallback; 25 | private boolean mCancelable = true; 26 | 27 | 28 | public Builder setTitle(@StringRes int title) { 29 | mDialogView.setTitle(title); 30 | return this; 31 | } 32 | 33 | public Builder setIcon(@DrawableRes int icon) { 34 | mDialogView.setIcon(icon); 35 | return this; 36 | } 37 | 38 | public Builder setMessage(CharSequence msg) { 39 | mDialogView.setMessage(msg); 40 | return this; 41 | } 42 | 43 | public Builder setPositive(CharSequence text) { 44 | mDialogView.setPositive(text); 45 | return this; 46 | } 47 | 48 | public Builder setNegative(CharSequence text) { 49 | mDialogView.setNegative(text); 50 | return this; 51 | } 52 | 53 | public Builder setRemember(CharSequence text) { 54 | mDialogView.setRemember(text); 55 | return this; 56 | } 57 | 58 | public Builder setCancelable(boolean cancelable) { 59 | mCancelable = cancelable; 60 | return this; 61 | } 62 | 63 | public Builder setCallback(OnRequestFinishListener callback) { 64 | mCallback = callback; 65 | return this; 66 | } 67 | 68 | public RequestDialog create() { 69 | return new RequestDialog(this); 70 | } 71 | 72 | public RequestDialog show() { 73 | RequestDialog dialog = new RequestDialog(this); 74 | dialog.show(); 75 | return dialog; 76 | } 77 | } 78 | 79 | 80 | protected RequestDialog(Builder builder) { 81 | super(builder.mActivity, android.R.style 82 | .Theme_Material_Light_Dialog_NoActionBar_MinWidth); 83 | 84 | setCancelable(builder.mCancelable); 85 | setContentView(builder.mDialogView, new ViewGroup.LayoutParams( 86 | ViewGroup.LayoutParams.MATCH_PARENT, 87 | ViewGroup.LayoutParams.WRAP_CONTENT 88 | )); 89 | 90 | ClickWrapper click = new ClickWrapper(); 91 | setOnCancelListener(click); 92 | builder.mDialogView.mNegativeView.setOnClickListener(click); 93 | builder.mDialogView.mPositiveView.setOnClickListener(click); 94 | 95 | 96 | mDialogView = builder.mDialogView; 97 | mCallback = builder.mCallback; 98 | } 99 | 100 | private final OnRequestFinishListener mCallback; 101 | private final AlertDialogLayout mDialogView; 102 | 103 | 104 | private final class ClickWrapper implements 105 | DialogInterface.OnCancelListener, 106 | View.OnClickListener { 107 | 108 | private boolean mShouldCallback = true; 109 | 110 | @Override 111 | public void onCancel(DialogInterface dialog) { 112 | dialog.dismiss(); 113 | onRequestFinish(OnRequestFinishListener.RESULT_CANCEL); 114 | } 115 | 116 | @Override 117 | public void onClick(View v) { 118 | dismiss(); 119 | 120 | if (v == mDialogView.mPositiveView) { 121 | onRequestFinish(OnRequestFinishListener.RESULT_POSITIVE); 122 | } 123 | else if (v == mDialogView.mNegativeView) { 124 | onRequestFinish(OnRequestFinishListener.RESULT_NEGATIVE); 125 | } 126 | } 127 | 128 | 129 | private void onRequestFinish(int result) { 130 | if (! mShouldCallback) { 131 | return; 132 | } 133 | mShouldCallback = false; 134 | 135 | if (mDialogView.mRememberLayout.getVisibility() == View.VISIBLE 136 | && mDialogView.mCheckBox.isChecked()) { 137 | result |= OnRequestFinishListener.RESULT_REMEMBER; 138 | } 139 | 140 | if (mCallback != null) { 141 | // mCallback.onRequestFinish(result); 142 | } 143 | } 144 | } 145 | 146 | 147 | public void setMessage(CharSequence text) { 148 | mDialogView.setMessage(text); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/ui/request/SystemRequestDialog.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.ui.request; 2 | 3 | import android.app.AlertDialog; 4 | import android.app.Dialog; 5 | import android.content.ClipData; 6 | import android.content.Context; 7 | import android.content.DialogInterface; 8 | import android.telecom.Call; 9 | import android.text.TextUtils; 10 | import android.util.TypedValue; 11 | import android.view.ContextThemeWrapper; 12 | import android.view.Gravity; 13 | import android.view.View; 14 | import android.view.ViewGroup; 15 | import android.view.Window; 16 | import android.view.WindowManager; 17 | import android.widget.CheckBox; 18 | import android.widget.LinearLayout; 19 | import android.widget.TextView; 20 | 21 | import androidx.annotation.StyleRes; 22 | 23 | import cn.nlifew.clipmgr.util.DisplayUtils; 24 | 25 | public class SystemRequestDialog extends AlertDialog { 26 | private static final String TAG = "RequestDialog2"; 27 | 28 | private static final @StyleRes int THEME = android.R.style 29 | .Theme_Material_Light_Dialog_NoActionBar_MinWidth; 30 | 31 | public SystemRequestDialog(Context context) { 32 | super(context, THEME); 33 | 34 | applyFlags(this); 35 | } 36 | 37 | private LinearLayout mRememberLayout; 38 | private CheckBox mCheckBox; 39 | private TextView mRememberView; 40 | 41 | private static void applyFlags(Dialog dialog) { 42 | final Window window = dialog.getWindow(); 43 | 44 | window.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); 45 | window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM 46 | | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); 47 | 48 | WindowManager.LayoutParams attrs = window.getAttributes(); 49 | attrs.setTitle("ClipMgrBridge"); 50 | 51 | window.setAttributes(attrs); 52 | } 53 | 54 | 55 | public void setRemember(CharSequence text) { 56 | if (TextUtils.isEmpty(text)) { 57 | if (mRememberLayout != null) { 58 | mRememberLayout.setVisibility(View.GONE); 59 | } 60 | return; 61 | } 62 | 63 | if (mRememberLayout == null) { 64 | Context context = new ContextThemeWrapper(getContext(), THEME); 65 | makeRememberLayout(context); 66 | setView(mRememberLayout); // NOT setContentView() !!! 67 | } 68 | mRememberLayout.setVisibility(View.VISIBLE); 69 | mRememberView.setText(text); 70 | } 71 | 72 | public boolean isRememberChecked() { 73 | return mRememberLayout != null 74 | && mRememberLayout.getVisibility() == View.VISIBLE 75 | && mCheckBox.isChecked(); 76 | } 77 | 78 | 79 | private void makeRememberLayout(Context context) { 80 | mRememberLayout = new LinearLayout(context); 81 | mRememberLayout.setOrientation(LinearLayout.HORIZONTAL); 82 | mRememberLayout.setVisibility(View.GONE); 83 | 84 | int DP24 = DisplayUtils.dp2px(context, 20); 85 | int DP10 = DisplayUtils.dp2px(context, 10); 86 | mRememberLayout.setPadding(DP24, DP10, DP24, DP10); 87 | 88 | LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( 89 | ViewGroup.LayoutParams.MATCH_PARENT, 90 | ViewGroup.LayoutParams.WRAP_CONTENT); 91 | lp.bottomMargin = DisplayUtils.dp2px(context, 5); 92 | mRememberLayout.setLayoutParams(lp); 93 | 94 | 95 | /* mCheckBox */ 96 | mCheckBox = new CheckBox(context); 97 | lp = new LinearLayout.LayoutParams( 98 | ViewGroup.LayoutParams.WRAP_CONTENT, 99 | ViewGroup.LayoutParams.MATCH_PARENT 100 | ); 101 | lp.rightMargin = DP10; 102 | mRememberLayout.addView(mCheckBox, lp); 103 | 104 | /* mRememberView */ 105 | mRememberView = new TextView(context); 106 | mRememberView.setGravity(Gravity.CENTER_VERTICAL); 107 | mRememberView.setTextColor(0xFF737373); 108 | mRememberView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); 109 | mRememberView.setOnClickListener(v -> mCheckBox.setChecked(! mCheckBox.isChecked())); 110 | 111 | lp = new LinearLayout.LayoutParams( 112 | ViewGroup.LayoutParams.WRAP_CONTENT, 113 | ViewGroup.LayoutParams.MATCH_PARENT); 114 | mRememberLayout.addView(mRememberView, lp); 115 | } 116 | 117 | 118 | 119 | 120 | public static class Callback implements OnRequestFinishListener, 121 | DialogInterface.OnClickListener, 122 | DialogInterface.OnCancelListener, 123 | DialogInterface.OnDismissListener { 124 | 125 | public Callback() { 126 | mCallback = this; 127 | } 128 | 129 | public Callback(OnRequestFinishListener callback) { 130 | mCallback = callback; 131 | } 132 | 133 | private boolean mShouldCallback = true; 134 | private final OnRequestFinishListener mCallback; 135 | 136 | @Override 137 | public void onCancel(DialogInterface dialog) { 138 | dialog.dismiss(); 139 | performCallback(dialog, OnRequestFinishListener.RESULT_CANCEL); 140 | } 141 | 142 | @Override 143 | public void onClick(DialogInterface dialog, int which) { 144 | dialog.dismiss(); 145 | switch (which) { 146 | case DialogInterface.BUTTON_POSITIVE: 147 | performCallback(dialog, RESULT_POSITIVE); 148 | break; 149 | case DialogInterface.BUTTON_NEGATIVE: 150 | performCallback(dialog, OnRequestFinishListener.RESULT_NEGATIVE); 151 | break; 152 | } 153 | } 154 | 155 | @Override 156 | public void onDismiss(DialogInterface dialog) { 157 | performCallback(dialog, RESULT_CANCEL); 158 | } 159 | 160 | private void performCallback(DialogInterface d, @flag int result) { 161 | if (!mShouldCallback) { 162 | return; 163 | } 164 | mShouldCallback = false; 165 | 166 | SystemRequestDialog dialog = (SystemRequestDialog) d; 167 | if (dialog.isRememberChecked()) { 168 | result |= RESULT_REMEMBER; 169 | } 170 | 171 | if (mCallback != null) { 172 | onRequestFinish(result); 173 | } 174 | } 175 | 176 | 177 | @Override 178 | public void onRequestFinish(int result) { 179 | 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/util/ClipUtils.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.util; 2 | 3 | import android.content.ClipData; 4 | import android.content.ClipboardManager; 5 | import android.content.Context; 6 | import android.util.Log; 7 | 8 | import java.util.Objects; 9 | 10 | public class ClipUtils { 11 | private static final String TAG = "ClipUtils"; 12 | 13 | 14 | public static String clip2String(ClipData clipData) { 15 | int n; 16 | if (clipData == null || (n = clipData.getItemCount()) == 0) { 17 | return ""; 18 | } 19 | StringBuilder sb = new StringBuilder(); 20 | for (int i = 0; i < n; i++) { 21 | ClipData.Item item = clipData.getItemAt(i); 22 | CharSequence text; 23 | if (item != null && (text = item.getText()) != null) { 24 | sb.append(text); 25 | } 26 | } 27 | return sb.toString(); 28 | } 29 | 30 | public static void clip2SimpleString(ClipData clipData, StringBuilder sb) { 31 | int n; 32 | if (clipData == null || sb == null || (n = clipData.getItemCount()) == 0) { 33 | return; 34 | } 35 | int length = sb.length() + 25; 36 | for (int i = 0; i < n; i++) { 37 | ClipData.Item item = clipData.getItemAt(i); 38 | CharSequence text; 39 | if (item != null && (text = item.getText()) != null 40 | && sb.append(text).length() >= length) { 41 | sb.setLength(length); 42 | sb.append("..."); 43 | return; 44 | } 45 | } 46 | } 47 | 48 | public static void setPrimaryClip(Context context, ClipData clip) { 49 | ClipboardManager cm = (ClipboardManager) context 50 | .getSystemService(Context.CLIPBOARD_SERVICE); 51 | if (cm == null) { 52 | Log.w(TAG, "setPrimaryClip: no ClipboardManager found"); 53 | } else { 54 | cm.setPrimaryClip(clip); 55 | } 56 | } 57 | 58 | public static boolean hasPrimaryClip(Context context, ClipData clip) { 59 | ClipboardManager cm = (ClipboardManager) context 60 | .getSystemService(Context.CLIPBOARD_SERVICE); 61 | return cm != null && equals(cm.getPrimaryClip(), clip); 62 | } 63 | 64 | 65 | public static boolean equals(ClipData d1, ClipData d2) { 66 | if (d1 == d2) { 67 | return true; 68 | } 69 | if (d1 == null || d2 == null) { 70 | return false; 71 | } 72 | // if (! Objects.equals(d1.getDescription(), d2.getDescription())) { 73 | // return false; 74 | // } 75 | return Objects.equals(d1.toString(), d2.toString()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/util/DirtyUtils.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.util; 2 | 3 | import android.app.Activity; 4 | import android.app.ActivityThread; 5 | import android.util.Log; 6 | 7 | import java.lang.reflect.Field; 8 | import java.util.Map; 9 | 10 | public class DirtyUtils { 11 | private static final String TAG = "DirtyUtils"; 12 | 13 | private static boolean sIgnoreGetTopActivity; 14 | 15 | private static Field sActivitiesField; 16 | private static Field sStoppedField; 17 | private static Field sActivityField; 18 | 19 | public static Activity getTopActivity() { 20 | if (sIgnoreGetTopActivity) { 21 | return null; 22 | } 23 | try { 24 | if (sActivitiesField == null) { 25 | sActivitiesField = ReflectUtils.getDeclaredField( 26 | ActivityThread.class, "mActivities" 27 | ); 28 | 29 | Class cls = Class.forName("android.app.ActivityThread$ActivityClientRecord"); 30 | sActivityField = ReflectUtils.getDeclaredField(cls, "activity"); 31 | sStoppedField = ReflectUtils.getDeclaredField(cls, "stopped"); 32 | } 33 | @SuppressWarnings("unchecked") 34 | final Map mActivities = (Map) 35 | sActivitiesField.get(ActivityThread.currentActivityThread()); 36 | 37 | for (Object r : mActivities.values()) { 38 | Activity activity = (Activity) sActivityField.get(r); 39 | if (activity.isFinishing() || activity.isDestroyed() 40 | || ((boolean) sStoppedField.get(r))) { 41 | continue; 42 | } 43 | return activity; 44 | } 45 | } catch (Exception e) { 46 | sIgnoreGetTopActivity = true; 47 | Log.e(TAG, "getTopActivity: ", e); 48 | } 49 | return null; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/util/DisplayUtils.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.util; 2 | 3 | import android.content.Context; 4 | 5 | public final class DisplayUtils { 6 | private DisplayUtils() {} 7 | 8 | public static int dp2px(Context c, float dp) { 9 | return (int) (c.getResources().getDisplayMetrics(). 10 | density * dp + 0.5f); 11 | } 12 | 13 | public static int sp2px(Context c, float sp) { 14 | return (int) (c.getResources().getDisplayMetrics(). 15 | scaledDensity * sp + 0.5f); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/util/PackageUtils.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.util; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.pm.ApplicationInfo; 7 | import android.content.pm.PackageManager; 8 | import android.net.Uri; 9 | 10 | import de.robv.android.xposed.XposedBridge; 11 | 12 | public final class PackageUtils { 13 | private static final String TAG = "PackageUtils"; 14 | 15 | private PackageUtils() { } 16 | 17 | 18 | public static void uninstall(Activity activity, String packageName) { 19 | Uri uri = Uri.fromParts("package", packageName, null); 20 | Intent intent = new Intent(Intent.ACTION_DELETE, uri); 21 | activity.startActivity(intent); 22 | } 23 | 24 | public static ApplicationInfo getApplicationInfo(PackageManager pm, String packageName) { 25 | try { 26 | return pm.getApplicationInfo(packageName, 0); 27 | } catch (PackageManager.NameNotFoundException e) { 28 | XposedBridge.log(TAG + ": getCallingApp: " + packageName); 29 | XposedBridge.log(e); 30 | } 31 | return null; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/util/ReflectUtils.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.util; 2 | 3 | import android.util.Log; 4 | 5 | import java.lang.reflect.Field; 6 | import java.lang.reflect.Method; 7 | 8 | public final class ReflectUtils { 9 | private static final String TAG = "ReflectUtils"; 10 | 11 | private ReflectUtils() { } 12 | 13 | public static Method getDeclaredMethod(Class cls, String name, Class... params) { 14 | Method method; 15 | try { 16 | method = cls.getDeclaredMethod(name, params); 17 | method.setAccessible(true); 18 | } catch (NoSuchMethodException e) { 19 | method = null; 20 | Log.e(TAG, "getDeclaredMethod: " + name, e); 21 | } 22 | return method; 23 | } 24 | 25 | public static Field getDeclaredField(Class cls, String name) { 26 | Field field; 27 | try { 28 | field = cls.getDeclaredField(name); 29 | field.setAccessible(true); 30 | } catch (NoSuchFieldException e) { 31 | field = null; 32 | Log.e(TAG, "getDeclaredField: " + name, e); 33 | } 34 | return field; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/cn/nlifew/clipmgr/util/ToastUtils.java: -------------------------------------------------------------------------------- 1 | package cn.nlifew.clipmgr.util; 2 | 3 | import android.content.Context; 4 | import android.widget.Toast; 5 | 6 | import androidx.annotation.StringRes; 7 | 8 | public final class ToastUtils { 9 | 10 | private static ToastUtils sInstance; 11 | 12 | public static ToastUtils getInstance(Context c) { 13 | if (sInstance == null) { 14 | synchronized (ToastUtils.class) { 15 | if (sInstance == null) { 16 | sInstance = new ToastUtils(c); 17 | } 18 | } 19 | } 20 | return sInstance; 21 | } 22 | 23 | private final Toast mToast; 24 | 25 | private ToastUtils(Context c) { 26 | mToast = Toast.makeText(c.getApplicationContext(), 27 | "", Toast.LENGTH_SHORT); 28 | } 29 | 30 | public void show(final @StringRes int text) { 31 | mToast.setText(text); 32 | mToast.show(); 33 | } 34 | 35 | public void show(final @StringRes int text, final int time) { 36 | mToast.setText(text); 37 | mToast.setDuration(time); 38 | mToast.show(); 39 | } 40 | 41 | public void show(final CharSequence text) { 42 | mToast.setText(text); 43 | mToast.show(); 44 | } 45 | 46 | public void show(final CharSequence text, final int time) { 47 | mToast.setText(text); 48 | mToast.setDuration(time); 49 | mToast.show(); 50 | } 51 | 52 | public Toast getToast() { 53 | return mToast; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nlifew/ClipMgr/6d78437995b7c12551f8fd9156eeccb001b3cfea/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nlifew/ClipMgr/6d78437995b7c12551f8fd9156eeccb001b3cfea/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nlifew/ClipMgr/6d78437995b7c12551f8fd9156eeccb001b3cfea/app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_btn.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nlifew/ClipMgr/6d78437995b7c12551f8fd9156eeccb001b3cfea/app/src/main/res/drawable/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 19 | 20 | 21 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_about_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 19 | 20 | 27 | 28 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_empty.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 |