Skip to content

Commit d1bcaff

Browse files
committed
feat: add app version check func hint
1 parent 6cbcc90 commit d1bcaff

10 files changed

Lines changed: 454 additions & 61 deletions

File tree

library/core/src/main/java/com/sevtinge/hyperceiler/dashboard/DashboardFragment.java

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@
3535
import androidx.core.view.MenuProvider;
3636
import androidx.preference.Preference;
3737

38-
import com.sevtinge.hyperceiler.core.R;
3938
import com.sevtinge.hyperceiler.common.log.AndroidLog;
39+
import com.sevtinge.hyperceiler.core.R;
40+
import com.sevtinge.hyperceiler.dashboard.DashboardFuncHintHelper.FuncHintRule;
41+
import com.sevtinge.hyperceiler.dashboard.DashboardFuncHintHelper.VersionRange;
4042
import com.sevtinge.hyperceiler.libhook.utils.pkg.CheckModifyUtils;
4143
import com.sevtinge.hyperceiler.utils.DialogHelper;
4244
import com.sevtinge.hyperceiler.utils.ThreadUtils;
@@ -53,10 +55,19 @@ public class DashboardFragment extends SettingsPreferenceFragment {
5355

5456
private static final String TAG = "DashboardFragment";
5557
private static final String APP_NS = "http://schemas.android.com/apk/res-auto";
58+
private static final String WARNING_BANNER_KEY = "prefs_key_app_version_warning";
59+
protected static final int APP_HINT_UNSUPPORTED = 1;
60+
protected static final int APP_HINT_SUPPORTED = 2;
61+
protected static final int APP_MATCH_OUT_OF_RANGE = 1;
62+
protected static final int APP_MATCH_IN_RANGE = 2;
5663

5764
// 静态缓存,避免每次进入子页面都重新解析 XML
5865
private static final Map<Integer, String> sQuickRestartCache = new ConcurrentHashMap<>();
5966

67+
private final DashboardPreferencePageLockHelper mPageLockHelper =
68+
new DashboardPreferencePageLockHelper(this, WARNING_BANNER_KEY);
69+
private final DashboardFuncHintHelper mFuncHintHelper =
70+
new DashboardFuncHintHelper(this, mPageLockHelper);
6071
private String mQuickRestartPackageName;
6172

6273
@Override
@@ -173,6 +184,54 @@ public void setFuncHint(Preference p, int value) {
173184
}
174185
}
175186

187+
public boolean setFuncHint(Preference p, int value, String pkgName, long minVersionCode, long maxVersionCode) {
188+
return mFuncHintHelper.setFuncHint(p, value, pkgName, minVersionCode, maxVersionCode);
189+
}
190+
191+
public boolean setFuncHint(Preference p, int value, int matchMode, String pkgName, long minVersionCode, long maxVersionCode) {
192+
return mFuncHintHelper.setFuncHint(p, value, matchMode, pkgName, minVersionCode, maxVersionCode);
193+
}
194+
195+
public boolean setFuncHint(Preference p, int value, String pkgName, long... ranges) {
196+
return mFuncHintHelper.setFuncHint(p, value, pkgName, ranges);
197+
}
198+
199+
public boolean setFuncHint(Preference p, int value, int matchMode, String pkgName, long... ranges) {
200+
return mFuncHintHelper.setFuncHint(p, value, matchMode, pkgName, ranges);
201+
}
202+
203+
public boolean setFuncHint(Preference p, int value, String pkgName, @Nullable VersionRange... ranges) {
204+
return mFuncHintHelper.setFuncHint(p, value, pkgName, ranges);
205+
}
206+
207+
public boolean setFuncHint(Preference p, int value, int matchMode, String pkgName, @Nullable VersionRange... ranges) {
208+
return mFuncHintHelper.setFuncHint(p, value, matchMode, pkgName, ranges);
209+
}
210+
211+
public void setFuncHints(String pkgName, @NonNull FuncHintRule... rules) {
212+
mFuncHintHelper.setFuncHints(pkgName, rules);
213+
}
214+
215+
protected static FuncHintRule rule(@NonNull Preference preference, int value, @Nullable VersionRange... ranges) {
216+
return DashboardFuncHintHelper.rule(preference, value, ranges);
217+
}
218+
219+
protected static FuncHintRule rule(@NonNull Preference preference, int value, int matchMode, @Nullable VersionRange... ranges) {
220+
return DashboardFuncHintHelper.rule(preference, value, matchMode, ranges);
221+
}
222+
223+
protected static VersionRange range(long minVersionCode, long maxVersionCode) {
224+
return DashboardFuncHintHelper.range(minVersionCode, maxVersionCode);
225+
}
226+
227+
protected static VersionRange atLeast(long minVersionCode) {
228+
return DashboardFuncHintHelper.atLeast(minVersionCode);
229+
}
230+
231+
protected static VersionRange atMost(long maxVersionCode) {
232+
return DashboardFuncHintHelper.atMost(maxVersionCode);
233+
}
234+
176235
public void setPreVisible(Preference p, boolean b) {
177236
if (!b) {
178237
cleanKey(p.getKey());
@@ -183,7 +242,15 @@ public void setPreVisible(Preference p, boolean b) {
183242
public void setAppModWarn(Preference p, String pkgName) {
184243
boolean check = CheckModifyUtils.INSTANCE.getCheckResult(getContext(), pkgName);
185244
boolean isDebugMode = getSharedPreferences().getBoolean("prefs_key_development_debug_mode", false);
186-
boolean isDebugVersion = getSharedPreferences().getInt("prefs_key_debug_choose_" + pkgName, 0) == 0;
187-
p.setVisible(check && !isDebugMode && isDebugVersion);
245+
int debugVersionCode = getSharedPreferences().getInt("debug_choose_" + pkgName, 0);
246+
if (debugVersionCode <= 0) {
247+
debugVersionCode = getSharedPreferences().getInt("prefs_key_debug_choose_" + pkgName, 0);
248+
}
249+
boolean isDebugVersion = debugVersionCode == 0;
250+
boolean showWarning = check && !isDebugMode && isDebugVersion;
251+
p.setVisible(showWarning);
252+
if (showWarning) {
253+
mPageLockHelper.lock(p, null);
254+
}
188255
}
189256
}
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
/*
2+
* This file is part of HyperCeiler.
3+
4+
* HyperCeiler is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU Affero General Public License as
6+
* published by the Free Software Foundation, either version 3 of the
7+
* License.
8+
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Affero General Public License for more details.
13+
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
17+
* Copyright (C) 2023-2026 HyperCeiler Contributions
18+
*/
19+
package com.sevtinge.hyperceiler.dashboard;
20+
21+
import android.content.pm.PackageInfo;
22+
import android.content.pm.PackageManager;
23+
import android.text.TextUtils;
24+
25+
import androidx.annotation.NonNull;
26+
import androidx.annotation.Nullable;
27+
import androidx.preference.Preference;
28+
29+
import com.sevtinge.hyperceiler.common.log.AndroidLog;
30+
import com.sevtinge.hyperceiler.core.R;
31+
32+
import java.util.Map;
33+
import java.util.concurrent.ConcurrentHashMap;
34+
35+
final class DashboardFuncHintHelper {
36+
private static final String TAG = "DashboardFuncHintHelper";
37+
private static final long UNKNOWN_VERSION_CODE = -1L;
38+
39+
private final DashboardFragment mHost;
40+
private final DashboardPreferencePageLockHelper mPageLockHelper;
41+
private final Map<String, Long> mVersionCodeCache = new ConcurrentHashMap<>();
42+
43+
DashboardFuncHintHelper(@NonNull DashboardFragment host, @NonNull DashboardPreferencePageLockHelper pageLockHelper) {
44+
mHost = host;
45+
mPageLockHelper = pageLockHelper;
46+
}
47+
48+
boolean setFuncHint(@NonNull Preference preference, int value, @Nullable String pkgName, long... ranges) {
49+
VersionRange[] parsedRanges = parseRanges(ranges);
50+
long versionCode = getVersionCode(pkgName);
51+
return applyRule(FuncHintRule.of(preference, value, DashboardFragment.APP_MATCH_OUT_OF_RANGE, parsedRanges), versionCode);
52+
}
53+
54+
boolean setFuncHint(@NonNull Preference preference, int value, int matchMode, @Nullable String pkgName, long... ranges) {
55+
VersionRange[] parsedRanges = parseRanges(ranges);
56+
long versionCode = getVersionCode(pkgName);
57+
return applyRule(FuncHintRule.of(preference, value, matchMode, parsedRanges), versionCode);
58+
}
59+
60+
boolean setFuncHint(@NonNull Preference preference, int value, @Nullable String pkgName, @Nullable VersionRange... ranges) {
61+
long versionCode = getVersionCode(pkgName);
62+
return applyRule(FuncHintRule.of(preference, value, DashboardFragment.APP_MATCH_OUT_OF_RANGE, ranges), versionCode);
63+
}
64+
65+
boolean setFuncHint(@NonNull Preference preference, int value, int matchMode, @Nullable String pkgName, @Nullable VersionRange... ranges) {
66+
long versionCode = getVersionCode(pkgName);
67+
return applyRule(FuncHintRule.of(preference, value, matchMode, ranges), versionCode);
68+
}
69+
70+
void setFuncHints(@Nullable String pkgName, @NonNull FuncHintRule... rules) {
71+
long versionCode = getVersionCode(pkgName);
72+
for (FuncHintRule rule : rules) {
73+
if (rule == null || rule.preference == null) continue;
74+
applyRule(rule, versionCode);
75+
}
76+
}
77+
78+
private boolean applyRule(@NonNull FuncHintRule rule, long versionCode) {
79+
Preference preference = rule.preference;
80+
if (versionCode == UNKNOWN_VERSION_CODE) {
81+
mPageLockHelper.lockWithVersionUnavailableWarning();
82+
disablePreference(preference);
83+
return false;
84+
}
85+
86+
boolean inRange = isInVersionRange(versionCode, rule.versionRanges);
87+
boolean shouldDisable = rule.matchMode == DashboardFragment.APP_MATCH_IN_RANGE ? inRange : !inRange;
88+
if (!shouldDisable) {
89+
return preference.isEnabled();
90+
}
91+
92+
disablePreference(preference);
93+
applySummary(preference, rule.value);
94+
return false;
95+
}
96+
97+
private void applySummary(@NonNull Preference preference, int value) {
98+
switch (value) {
99+
case DashboardFragment.APP_HINT_UNSUPPORTED ->
100+
preference.setSummary(R.string.unsupported_app_version_func);
101+
case DashboardFragment.APP_HINT_SUPPORTED ->
102+
preference.setSummary(R.string.supported_app_version_func);
103+
default ->
104+
throw new IllegalStateException("Unexpected app version summary value: " + value);
105+
}
106+
}
107+
108+
private void disablePreference(@NonNull Preference preference) {
109+
String key = preference.getKey();
110+
if (!TextUtils.isEmpty(key)) {
111+
mHost.cleanKey(key);
112+
}
113+
preference.setEnabled(false);
114+
}
115+
116+
private long getVersionCode(@Nullable String pkgName) {
117+
if (TextUtils.isEmpty(pkgName)) {
118+
return UNKNOWN_VERSION_CODE;
119+
}
120+
Long cached = mVersionCodeCache.get(pkgName);
121+
if (cached != null) {
122+
return cached;
123+
}
124+
125+
long versionCode = queryVersionCode(pkgName);
126+
mVersionCodeCache.put(pkgName, versionCode);
127+
return versionCode;
128+
}
129+
130+
private long queryVersionCode(@NonNull String pkgName) {
131+
try {
132+
boolean isDebugMode = mHost.getSharedPreferences().getBoolean("prefs_key_development_debug_mode", false);
133+
int debugVersionCode = mHost.getSharedPreferences().getInt("debug_choose_" + pkgName, 0);
134+
if (debugVersionCode <= 0) {
135+
debugVersionCode = mHost.getSharedPreferences().getInt("prefs_key_debug_choose_" + pkgName, 0);
136+
}
137+
if (isDebugMode && debugVersionCode > 0) {
138+
return debugVersionCode;
139+
}
140+
141+
PackageInfo packageInfo = mHost.requireContext().getPackageManager().getPackageInfo(pkgName, PackageManager.MATCH_ALL);
142+
long versionCode = packageInfo.getLongVersionCode();
143+
return versionCode > 0 ? versionCode : UNKNOWN_VERSION_CODE;
144+
} catch (PackageManager.NameNotFoundException e) {
145+
AndroidLog.w(TAG, "Failed to find package while querying version: " + pkgName, e);
146+
} catch (Exception e) {
147+
AndroidLog.e(TAG, "Failed to query package version: " + pkgName, e);
148+
}
149+
return UNKNOWN_VERSION_CODE;
150+
}
151+
152+
private boolean isInVersionRange(long versionCode, @Nullable VersionRange... ranges) {
153+
if (ranges == null || ranges.length == 0) return true;
154+
for (VersionRange range : ranges) {
155+
if (range == null) continue;
156+
boolean matchMin = range.minVersionCode <= 0 || versionCode >= range.minVersionCode;
157+
boolean matchMax = range.maxVersionCode <= 0 || versionCode <= range.maxVersionCode;
158+
if (matchMin && matchMax) return true;
159+
}
160+
return false;
161+
}
162+
163+
static FuncHintRule rule(@NonNull Preference preference, int value, @Nullable VersionRange... ranges) {
164+
return FuncHintRule.of(preference, value, DashboardFragment.APP_MATCH_OUT_OF_RANGE, ranges);
165+
}
166+
167+
static FuncHintRule rule(@NonNull Preference preference, int value, int matchMode, @Nullable VersionRange... ranges) {
168+
return FuncHintRule.of(preference, value, matchMode, ranges);
169+
}
170+
171+
static VersionRange range(long minVersionCode, long maxVersionCode) {
172+
return VersionRange.between(minVersionCode, maxVersionCode);
173+
}
174+
175+
static VersionRange atLeast(long minVersionCode) {
176+
return VersionRange.atLeast(minVersionCode);
177+
}
178+
179+
static VersionRange atMost(long maxVersionCode) {
180+
return VersionRange.atMost(maxVersionCode);
181+
}
182+
183+
private static VersionRange[] parseRanges(@Nullable long... ranges) {
184+
if (ranges == null || ranges.length == 0) {
185+
return new VersionRange[0];
186+
}
187+
if (ranges.length % 2 != 0) {
188+
throw new IllegalStateException("Version ranges must be min/max pairs.");
189+
}
190+
191+
VersionRange[] parsed = new VersionRange[ranges.length / 2];
192+
int idx = 0;
193+
for (int i = 0; i < ranges.length; i += 2) {
194+
parsed[idx++] = VersionRange.between(ranges[i], ranges[i + 1]);
195+
}
196+
return parsed;
197+
}
198+
199+
public static final class FuncHintRule {
200+
final Preference preference;
201+
final int value;
202+
final int matchMode;
203+
final VersionRange[] versionRanges;
204+
205+
private FuncHintRule(@NonNull Preference preference, int value, int matchMode, @Nullable VersionRange[] versionRanges) {
206+
this.preference = preference;
207+
this.value = value;
208+
this.matchMode = matchMode;
209+
this.versionRanges = versionRanges;
210+
}
211+
212+
public static FuncHintRule of(@NonNull Preference preference, int value, long... ranges) {
213+
return of(preference, value, DashboardFragment.APP_MATCH_OUT_OF_RANGE, parseRanges(ranges));
214+
}
215+
216+
public static FuncHintRule of(@NonNull Preference preference, int value, @Nullable VersionRange... ranges) {
217+
return of(preference, value, DashboardFragment.APP_MATCH_OUT_OF_RANGE, ranges);
218+
}
219+
220+
public static FuncHintRule of(@NonNull Preference preference, int value, int matchMode, long... ranges) {
221+
return of(preference, value, matchMode, parseRanges(ranges));
222+
}
223+
224+
public static FuncHintRule of(@NonNull Preference preference, int value, int matchMode, @Nullable VersionRange... ranges) {
225+
return new FuncHintRule(preference, value, matchMode, ranges);
226+
}
227+
}
228+
229+
public static final class VersionRange {
230+
final long minVersionCode;
231+
final long maxVersionCode;
232+
233+
private VersionRange(long minVersionCode, long maxVersionCode) {
234+
this.minVersionCode = minVersionCode;
235+
this.maxVersionCode = maxVersionCode;
236+
}
237+
238+
public static VersionRange between(long minVersionCode, long maxVersionCode) {
239+
return new VersionRange(minVersionCode, maxVersionCode);
240+
}
241+
242+
public static VersionRange atLeast(long minVersionCode) {
243+
return new VersionRange(minVersionCode, 0);
244+
}
245+
246+
public static VersionRange atMost(long maxVersionCode) {
247+
return new VersionRange(0, maxVersionCode);
248+
}
249+
}
250+
}

0 commit comments

Comments
 (0)