[FEAT] 效期過期商品自動下架(暫不販售)+ 訊號權限 root 自授權規格
1. SlotStructure 新增 isExpired()/getExpiryDate()/clearExpiry():以 ISO 日期字串字典序比對效期(避開時區/解析誤差),當天可賣、隔天起才鎖;解析失敗 fail-open 不擋售,避免時鐘或資料異常誤鎖全機。 2. VMC 銷售清單與貨道清單:過期商品顯示「暫不販售」遮罩並禁止加入購物車/購買(recycler_vmc_slot_list.xml + RecyclerVmcBuyListAdpter/RecyclerVmcSlotListAdpter)。 3. 出貨前即時效期複查:DialogProductDetail/DispatchDialog/MemberCardPickupDialog/CartViewModel 於出貨當下再驗一次,過期則取消出貨並返回銷售平台。 4. 貨道庫存歸 0(賣完)時自動清除效期與批號,避免舊批效期殘留導致補新貨被誤鎖。 5. 新增多語系字串 product_expired「暫不販售」(11 語系)。 6. docs:network-signal-reporting-spec 補上「機台 rooted → App 啟動時以 su 自我授權 ACCESS_FINE_LOCATION」的定案做法(失敗靜默、不影響其他功能)。 7. 版號遞增 verPatch 17→18(versionName 21_02_18_D / versionCode 210218)供 OTA 辨識。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ed9b05636c
commit
8db3fab1c0
@ -10,7 +10,7 @@ plugins {
|
||||
// 避免不同尺寸機台間版本號產生衝突。
|
||||
// =========================================================================
|
||||
def verMinor = 2
|
||||
def verPatch = 16
|
||||
def verPatch = 18
|
||||
|
||||
|
||||
|
||||
|
||||
@ -100,7 +100,57 @@ public class SlotStructure {
|
||||
return otherData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 效期是否已過期(後台庫存同步透過 otherData.expiry_date 下發,格式 "yyyy-MM-dd")。
|
||||
* 規則:當天仍可販售,今天 > 效期日(即隔天起)才視為過期。
|
||||
* 採 ISO 日期字串字典序比對(等同時間序),避開時區/解析誤差。
|
||||
* 失敗時 fail-open 回傳 false(不擋售),避免時鐘或資料異常誤鎖全機。
|
||||
*/
|
||||
public boolean isExpired() {
|
||||
if (otherData == null || !otherData.has("expiry_date"))
|
||||
return false;
|
||||
try {
|
||||
String expiry = otherData.getString("expiry_date");
|
||||
if (expiry == null || expiry.isEmpty() || "null".equalsIgnoreCase(expiry))
|
||||
return false;
|
||||
// 僅取日期部分 yyyy-MM-dd(即使後台帶了時間也安全)
|
||||
String expiryDate = expiry.length() >= 10 ? expiry.substring(0, 10) : expiry;
|
||||
String today = new java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.US)
|
||||
.format(new java.util.Date());
|
||||
return today.compareTo(expiryDate) > 0;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void setOtherData(JSONObject otherData) {
|
||||
this.otherData = otherData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得效期字串(yyyy-MM-dd),無設定時回傳空字串。供貨道管理 UI 顯示用。
|
||||
*/
|
||||
public String getExpiryDate() {
|
||||
if (otherData == null || !otherData.has("expiry_date"))
|
||||
return "";
|
||||
try {
|
||||
String expiry = otherData.getString("expiry_date");
|
||||
if (expiry == null || "null".equalsIgnoreCase(expiry))
|
||||
return "";
|
||||
return expiry.length() >= 10 ? expiry.substring(0, 10) : expiry;
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除效期與批號。通常在貨道庫存歸 0(賣完)時呼叫,避免舊批貨效期殘留,
|
||||
* 導致下次本機補新貨時被舊效期誤鎖成「暫不販售」。
|
||||
*/
|
||||
public void clearExpiry() {
|
||||
if (otherData == null)
|
||||
return;
|
||||
otherData.remove("expiry_date");
|
||||
otherData.remove("batch_no");
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,6 +89,7 @@ public class RecyclerVmcBuyListAdpter extends RecyclerView.Adapter<RecyclerVmcBu
|
||||
int slotUpperLimit = slot.getProduct().getSlotUpperLimit();
|
||||
int count = slot.getCount();
|
||||
boolean isLock = slot.isLock();
|
||||
boolean isExpired = slot.isExpired();
|
||||
|
||||
int price = machinePrice > 0 ? machinePrice : sellingPrice;
|
||||
holder.binding.textPrice.setVisibility(View.VISIBLE);
|
||||
@ -105,12 +106,18 @@ public class RecyclerVmcBuyListAdpter extends RecyclerView.Adapter<RecyclerVmcBu
|
||||
holder.binding.textMask.setText("售完");
|
||||
}
|
||||
|
||||
// 效期過期:顯示「暫不販售」遮罩,並一律禁止購買(蓋過售完顯示,主要情境是有貨但過效期)
|
||||
if (isExpired) {
|
||||
holder.binding.mask.setVisibility(View.VISIBLE);
|
||||
holder.binding.textMask.setText(context.getString(R.string.product_expired));
|
||||
}
|
||||
|
||||
if (isLock) {
|
||||
holder.binding.mask.setVisibility(View.VISIBLE);
|
||||
holder.binding.textMask.setText("暫停販售");
|
||||
}
|
||||
|
||||
if (count > 0 && !isLock) {
|
||||
if (count > 0 && !isLock && !isExpired) {
|
||||
holder.binding.imageProductPicture.setFocusable(false);
|
||||
holder.binding.imageProductPicture.setFocusableInTouchMode(false);
|
||||
holder.binding.imageProductPicture.setOnClickListener(new imageProductPictureOnClickListener(position));
|
||||
@ -123,7 +130,7 @@ public class RecyclerVmcBuyListAdpter extends RecyclerView.Adapter<RecyclerVmcBu
|
||||
|
||||
// 商品卡右下角「+」快速加購:僅購物車功能開啟時顯示;無購物車時隱藏(只能點商品開詳情頁)
|
||||
boolean cartOn = MyApp.getInstance().getDevSet().isShoppingCar();
|
||||
if (cartOn && count > 0 && !isLock) {
|
||||
if (cartOn && count > 0 && !isLock && !isExpired) {
|
||||
holder.binding.buttonQuickAdd.setVisibility(View.VISIBLE);
|
||||
holder.binding.buttonQuickAdd.setOnClickListener(v -> {
|
||||
if (MyApp.getInstance().isMachineBusy()) return;
|
||||
@ -177,6 +184,15 @@ public class RecyclerVmcBuyListAdpter extends RecyclerView.Adapter<RecyclerVmcBu
|
||||
VmcFragment.Option.SHIPPED_PRODUCT.getOption(), "refresh");
|
||||
return;
|
||||
}
|
||||
// ✅ 即時效期檢查:過期商品一律暫不販售
|
||||
if (liveSlot != null && liveSlot.isExpired()) {
|
||||
logs.info("即時效期檢查:商品已過效期 slot=" + slotProduct.getSlot());
|
||||
android.widget.Toast.makeText(context, context.getString(R.string.product_expired),
|
||||
android.widget.Toast.LENGTH_SHORT).show();
|
||||
fragment.getSrcHandlerMain().start(getClass().getSimpleName(),
|
||||
VmcFragment.Option.SHIPPED_PRODUCT.getOption(), "refresh");
|
||||
return;
|
||||
}
|
||||
} catch (LogsEmptyException e) {
|
||||
// 查詢失敗時不阻擋,讓原流程繼續
|
||||
}
|
||||
|
||||
@ -76,6 +76,21 @@ public class RecyclerVmcSlotListAdpter extends RecyclerView.Adapter<RecyclerVmcS
|
||||
// numberPassword 預設會遮罩輸入,關閉遮罩以顯示明碼數字
|
||||
holder.binding.editCount.setTransformationMethod(null);
|
||||
|
||||
// 效期顯示:有設定效期才顯示;已過期以紅字標示,方便維護人員辨識
|
||||
String expiry = slot.getExpiryDate();
|
||||
if (expiry.isEmpty()) {
|
||||
holder.binding.textExpiry.setVisibility(View.GONE);
|
||||
} else {
|
||||
holder.binding.textExpiry.setVisibility(View.VISIBLE);
|
||||
if (slot.isExpired()) {
|
||||
holder.binding.textExpiry.setText("效期 " + expiry + "(已過期)");
|
||||
holder.binding.textExpiry.setTextColor(androidx.core.content.ContextCompat.getColor(context, R.color.warning));
|
||||
} else {
|
||||
holder.binding.textExpiry.setText("效期 " + expiry);
|
||||
holder.binding.textExpiry.setTextColor(androidx.core.content.ContextCompat.getColor(context, R.color.black));
|
||||
}
|
||||
}
|
||||
|
||||
if (slot.isLock()) {
|
||||
holder.binding.textLock.setChecked(false);
|
||||
holder.binding.textLockStatus.setText("停售中");
|
||||
|
||||
@ -153,6 +153,12 @@ public class DialogProductDetail extends DialogAbstract {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
// 效期過期商品一律暫不販售
|
||||
if (slot.isExpired()) {
|
||||
ToastUtils.showShort(R.string.product_expired);
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
addSelectedQuantityToCart();
|
||||
ToastUtils.showShort(R.string.cart_added);
|
||||
cancel();
|
||||
|
||||
@ -936,6 +936,11 @@ public class DispatchDialog extends DialogAbstract {
|
||||
int slotCount = MyApp.getInstance().getSlotData(field, slot).getCount();
|
||||
remaining = slotCount - increase; // 計算剩餘數量
|
||||
MyApp.getInstance().getSlotData(field, slot).setCount(remaining);
|
||||
// 賣完(庫存歸 0)即清除效期/批號,避免舊批貨效期殘留,導致下次補新貨被舊效期誤鎖
|
||||
if (remaining <= 0) {
|
||||
MyApp.getInstance().getSlotData(field, slot).clearExpiry();
|
||||
getLogs().info("貨道 " + slot + " 庫存歸 0,已清除效期資料。");
|
||||
}
|
||||
MyApp.getInstance().getReportDispatchInfoData().setRemaining(remaining);
|
||||
} catch (LogsEmptyException e) {
|
||||
getLogs().warning(e);
|
||||
|
||||
@ -205,6 +205,12 @@ public class MemberCardPickupDialog extends DialogAbstract {
|
||||
dialogCancel(); // 內部已清空 BuyList + dismiss
|
||||
return;
|
||||
}
|
||||
if (liveSlot != null && liveSlot.isExpired()) {
|
||||
getLogs().info("商品已過效期 slot=" + buy.getSlot() + ",取消出貨,返回銷售平台。");
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.TOAST.getOption(), "商品已過效期,暫不販售");
|
||||
dialogCancel(); // 內部已清空 BuyList + dismiss
|
||||
return;
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
|
||||
@ -48,6 +48,11 @@ class CartViewModel : ViewModel() {
|
||||
|
||||
// 添加商品到購物車(以 field + slot + productID 唯一識別)
|
||||
fun addToCart(slot: SlotStructure): CartOperationResult {
|
||||
// 效期過期商品一律暫不販售,禁止加入購物車
|
||||
if (slot.isExpired) {
|
||||
ToastUtils.showLong(R.string.product_expired)
|
||||
return createResult(null)
|
||||
}
|
||||
val currentItems = _cartItems.value?.toMutableList() ?: mutableListOf()
|
||||
val item = currentItems.find {
|
||||
it.field == slot.field &&
|
||||
|
||||
@ -102,6 +102,21 @@
|
||||
app:layout_constraintTop_toBottomOf="@id/imageProduct"
|
||||
android:layout_marginTop="0dp" />
|
||||
|
||||
<!-- 效期顯示(無設定時隱藏;已過期由 Adapter 改紅字提示) -->
|
||||
<TextView
|
||||
android:id="@+id/textExpiry"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textSize="13sp"
|
||||
android:textColor="@color/black"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:text=""
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/textProductName"
|
||||
android:layout_marginTop="2dp" />
|
||||
|
||||
<!-- 第四列:庫存數量控制列 - Minus -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/buttonCountMinus"
|
||||
@ -113,7 +128,7 @@
|
||||
android:textSize="30sp"
|
||||
app:backgroundTint="@color/background"
|
||||
android:textColor="@color/primary"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textProductName"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textExpiry"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/textCount" />
|
||||
|
||||
|
||||
@ -288,6 +288,7 @@
|
||||
<string name="time_left">Restzeit 00H:00M</string>
|
||||
<string name="expires_at">Ablauf 0000/00/00 00H:00M</string>
|
||||
<string name="product_sold_out">Produkt ausverkauft</string>
|
||||
<string name="product_expired">Nicht verfügbar</string>
|
||||
<!-- recycler_electric_slot_list.xml -->
|
||||
<string name="lane_number">Schachtnummer</string>
|
||||
<string name="amount">Menge</string>
|
||||
|
||||
@ -288,6 +288,7 @@
|
||||
<string name="time_left">Time left 00H:00M</string>
|
||||
<string name="expires_at">Expires at 0000/00/00 00H:00M</string>
|
||||
<string name="product_sold_out">Product sold out</string>
|
||||
<string name="product_expired">Unavailable</string>
|
||||
<!-- recycler_electric_slot_list.xml -->
|
||||
<string name="lane_number">Lane Number</string>
|
||||
<string name="amount">Amount</string>
|
||||
|
||||
@ -288,6 +288,7 @@
|
||||
<string name="time_left">Tiempo restante 00H:00M</string>
|
||||
<string name="expires_at">Vence el 0000/00/00 00H:00M</string>
|
||||
<string name="product_sold_out">Producto agotado</string>
|
||||
<string name="product_expired">No disponible</string>
|
||||
<!-- recycler_electric_slot_list.xml -->
|
||||
<string name="lane_number">Número de canal</string>
|
||||
<string name="amount">Cantidad</string>
|
||||
|
||||
@ -288,6 +288,7 @@
|
||||
<string name="time_left">Temps restant 00H:00M</string>
|
||||
<string name="expires_at">Expire le 0000/00/00 00H:00M</string>
|
||||
<string name="product_sold_out">Produit épuisé</string>
|
||||
<string name="product_expired">Indisponible</string>
|
||||
<!-- recycler_electric_slot_list.xml -->
|
||||
<string name="lane_number">Numéro de couloir</string>
|
||||
<string name="amount">Quantité</string>
|
||||
|
||||
@ -288,6 +288,7 @@
|
||||
<string name="time_left">Sisa waktu 00J:00M</string>
|
||||
<string name="expires_at">Berakhir pada 0000/00/00 00J:00M</string>
|
||||
<string name="product_sold_out">Produk habis terjual</string>
|
||||
<string name="product_expired">Tidak dijual</string>
|
||||
<!-- recycler_electric_slot_list.xml -->
|
||||
<string name="lane_number">Nomor Jalur</string>
|
||||
<string name="amount">Jumlah</string>
|
||||
|
||||
@ -288,6 +288,7 @@
|
||||
<string name="time_left">残り時間 00時:00分</string>
|
||||
<string name="expires_at">0000/00/00 00時:00分 期限</string>
|
||||
<string name="product_sold_out">商品は売り切れました</string>
|
||||
<string name="product_expired">販売停止</string>
|
||||
<!-- recycler_electric_slot_list.xml -->
|
||||
<string name="lane_number">車線番号</string>
|
||||
<string name="amount">量</string>
|
||||
|
||||
@ -288,6 +288,7 @@
|
||||
<string name="time_left">남은 시간 00H:00M</string>
|
||||
<string name="expires_at">만료 0000/00/00 00H:00M</string>
|
||||
<string name="product_sold_out">상품 매진</string>
|
||||
<string name="product_expired">판매 중지</string>
|
||||
<!-- recycler_electric_slot_list.xml -->
|
||||
<string name="lane_number">레인 번호</string>
|
||||
<string name="amount">수량</string>
|
||||
|
||||
@ -288,6 +288,7 @@
|
||||
<string name="time_left">เหลือเวลา 00H:00M</string>
|
||||
<string name="expires_at">หมดอายุ 0000/00/00 00H:00M</string>
|
||||
<string name="product_sold_out">สินค้าจำหน่ายหมดแล้ว</string>
|
||||
<string name="product_expired">งดจำหน่ายชั่วคราว</string>
|
||||
<!-- recycler_electric_slot_list.xml -->
|
||||
<string name="lane_number">หมายเลขช่องสินค้า</string>
|
||||
<string name="amount">จำนวน</string>
|
||||
|
||||
@ -288,6 +288,7 @@
|
||||
<string name="time_left">Còn lại 00H:00M</string>
|
||||
<string name="expires_at">Hết hạn lúc 0000/00/00 00H:00M</string>
|
||||
<string name="product_sold_out">Sản phẩm đã bán hết</string>
|
||||
<string name="product_expired">Tạm ngừng bán</string>
|
||||
<!-- recycler_electric_slot_list.xml -->
|
||||
<string name="lane_number">Số ngăn hàng</string>
|
||||
<string name="amount">Số lượng</string>
|
||||
|
||||
@ -288,6 +288,7 @@
|
||||
<string name="time_left">剩余 00时:00分</string>
|
||||
<string name="expires_at">0000/00/00 00时:00分 到期</string>
|
||||
<string name="product_sold_out">商品销售完毕</string>
|
||||
<string name="product_expired">暂不贩售</string>
|
||||
<!-- recycler_electric_slot_list.xml -->
|
||||
<string name="lane_number">货道编号</string>
|
||||
<string name="amount">数量</string>
|
||||
|
||||
@ -199,6 +199,7 @@
|
||||
<string name="time_left">剩餘 00時:00分</string>
|
||||
<string name="expires_at">0000/00/00 00時:00分 到期</string>
|
||||
<string name="product_sold_out">商品銷售完畢</string>
|
||||
<string name="product_expired">暫不販售</string>
|
||||
<!-- recycler_electric_slot_list.xml -->
|
||||
<string name="lane_number">貨道編號</string>
|
||||
<string name="amount">數量</string>
|
||||
|
||||
@ -72,9 +72,15 @@ int rsrq = (SDK_INT >= 29) ? lte.getRsrq() : Integer.MAX_VALUE;
|
||||
- 本專案 minSdk 21 = ART,採惰性驗證;對「**既有類別的新方法**」(如 `CellSignalStrengthLte.getRsrp()`,類別自 API 17 即存在)只要包 `Build.VERSION.SDK_INT` inline guard 就安全,**不會** class-load `VerifyError`/啟動崩潰。這也是本檔既有慣例([FontendActivity.java:709](file:///home/mama/projects/TaiwanStar_general_size_s_Chengwai/app/src/main/java/com/unibuy/smartdevice/ui/FontendActivity.java#L709) 的 `getSignalStrength()` API29、:1474 的 TIRAMISU API33 皆 inline 出貨)。Wrapper/Helper class 只是可選風格,非正確性必需。
|
||||
- **唯一例外**:引用 API 29+/30+ 才有的**新類別「型別」**(`CellInfoNr`/`CellSignalStrengthNr`)時,少數客製 ROM 仍可能 `NoClassDefFoundError`。故 **5G 分支整段務必在 `SDK_INT` guard 內**(見下點)。
|
||||
3. **5G NSA fallback(agy 查核)**:現場 5G 多為 **NSA(非獨立組網)**,控制面錨定在 LTE,`getAllCellInfo()` 主小區仍是 `CellInfoLte`。所以**不能只靠 `CellInfoNr`**;邏輯應為「先取 `CellInfoNr`(SA 5G,整段包在 API 31+ guard 內),取不到則 fallback `CellInfoLte`」,否則 NSA 機台拿不到訊號。
|
||||
4. **權限:真機需驗證 + 穩健性缺口(agy 詰問後降級,非確證致命 bug)**:`checkPermissionByAccessFineLocation()` 在 Android M+ 直接 `return true`,**沒做執行期動態申請**,且全專案無 `requestPermissions`、非平台簽章。但 `getMobileSignalStrength()` 有 `try-catch(SecurityException)` → 即使未授權也只是回 -1,**不會崩**。
|
||||
- 這些是**專用 kiosk 機**,`ACCESS_FINE_LOCATION` 多半在佈署時用 `adb shell pm grant` 或 provisioning 預授;且 cellular 分支只在 active transport==CELLULAR 時才跑(現場多走 WiFi/有線),故是否真壞**未經現場驗證**。
|
||||
- **行動**:實作訊號上報前,先在目標機台確認權限已授(`adb shell dumpsys package <pkg> | grep ACCESS_FINE_LOCATION`);若未授,選擇「佈署腳本 `pm grant`」或「App 內補執行期申請」其一即可。不列為阻斷性 bug,但**不確認就上線,行動網路訊號會全空**。
|
||||
4. **權限:App 用 root 自我授權(定案,適配「打包 APK 直裝機台、無 adb、無人值守」佈署模式)**:
|
||||
- 現況:`checkPermissionByAccessFineLocation()` 在 Android M+ 直接 `return true`,**沒做執行期動態申請**;App 非平台簽章/系統 App(debug key、無 `sharedUserId`)。但 `getMobileSignalStrength()` 有 `try-catch(SecurityException)` → 未授權也只回 -1,**不會崩**。
|
||||
- **關鍵:機台是 rooted**,App 已用 `su` 做乙太網路/OTA/RS232(見 [EthernetTetherHelper.java:10](file:///home/mama/projects/TaiwanStar_general_size_s_Chengwai/app/src/main/java/com/unibuy/smartdevice/worker/EthernetTetherHelper.java#L10)、[OtaManager.java:529](file:///home/mama/projects/TaiwanStar_general_size_s_Chengwai/app/src/main/java/com/unibuy/smartdevice/tools/OtaManager.java#L529))。因此**不需 adb、不需人工、不需跳視窗**。
|
||||
- **做法**:App 啟動時(讀訊號前)以 root 自我授權,照抄 `EthernetTetherHelper` 的 `exec("su")` 模式:
|
||||
```
|
||||
su -c "pm grant <context.getPackageName()> android.permission.ACCESS_FINE_LOCATION"
|
||||
```
|
||||
建議封裝成 `LocationPermissionHelper.ensureGrantedViaRoot()`,失敗靜默(訊號降為 -1,不影響其他功能)。授權具持久性,授一次即可,每次開機保險再跑無妨。
|
||||
- 備註:cellular 分支只在 active transport==CELLULAR 才跑(現場多走 WiFi/有線),故行動訊號上報主要在用 SIM 的機台才有資料。
|
||||
|
||||
### 1.3 commit 注意
|
||||
本分支 `feat/general-cart-and-payment` 正在改 `MqttManager`;`HeartbeatPayload` 擴充與訊號讀取請**獨立小 commit**,避免與購物車/支付改動纏在一起。
|
||||
@ -162,7 +168,7 @@ $table->timestamp('last_network_at')->nullable();
|
||||
|
||||
## 5. 實作清單(給後台同事 / 後續排程)
|
||||
**App 端**
|
||||
- [ ] **先驗權限**:在目標機台確認 `ACCESS_FINE_LOCATION` 已授(`adb shell dumpsys package <pkg> | grep FINE_LOCATION`);未授則 `pm grant` 或補執行期申請(§1.2-4,非阻斷但不確認會全空)
|
||||
- [ ] **權限自我授權**:新增 `LocationPermissionHelper.ensureGrantedViaRoot()`,啟動時以 `su -c "pm grant <pkg> ACCESS_FINE_LOCATION"` 靜默授權(照抄 `EthernetTetherHelper`,失敗不崩)(§1.2-4)
|
||||
- [ ] 訊號讀取:補 RSSI/SINR/RSRQ + inline `Build.VERSION.SDK_INT` 防護(符合本庫慣例,毋須 Wrapper class)+ `CellInfoNr`→`CellInfoLte` fallback、5G 分支整段在 API31+ guard 內(§1.2-2/3)
|
||||
- [ ] `HeartbeatPayload` 加巢狀 `network`(含對應 class,測 Gson JSON 結構;null 不序列化已驗證安全)
|
||||
- [ ] 獨立 commit(勿混入購物車/支付)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user