├── .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 |
6 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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