2023年3月29日 星期三

完整清除 Uninstall pCloud Drive app @ macOS 13.2.1


故事是這樣的,幾個月前實驗使用 Cryptomator + pCloud 備份,由於前者會將資料切個成常見的零碎的目錄結構,如果有檔案出現異常時,整個資料就無法讀取。

Error Code 4VHF:Q9JM:I1GP

最近 pCloud 很頻繁更新,再加上更新至 Cryptomator 1.7.3 ,很奇妙的,有一個高頻變動的目錄,開始出現無法讀取的現象。經過幾番追蹤後,確認是因為 Cryptomator + macFUSE 方案產生的現象,接著做檔案搬遷就會出錯,無論降低版本到 Cryptomator 1.6.17 還是升級 macFUSE 4.4.2,都沒法排除。

測試方式是建立新的加密庫,接著 rsync -avP 搬遷,皆會一陣子就出現 input/output error 。最後將 Cryptomator 更換成 WebDAV 模式後,才確定排除。且 Cryptomator 錯誤訊息介面可以快速跳到 github 4VHF:Q9JM:I1GP 相關討論區,也在講 macFUSE 的事情。排除問題後,就進入到這次的主題 pCloud app 的處理。

pCloud App Bug?沒有 synced folders 規則,還一直在同步

pCloud App Bug?沒有 synced folders 規則,添加時還顯示已在同步(已有規則)

重建完加密庫後,想要透過 pCloud app 同步資料時,發現整個 pCloud Drive app - macOS 進入到非常詭異的狀態,我只是把 pCloud app - Synced Folders 的目錄透過介面打 X 取消指定目錄同步,下一刻我發現本地的目錄被刪除了,對應的 pCloud 雲端上的資料也被刪除。這件事其實還滿嚴重的。

當初對 pCloud 的用法也是處於體驗,純粹當作另一處備份區,想說資料不見也沒差再重建就好。殊不知在本地重建時, pCloud app 一直還是不斷的 sync 指定目錄,且同步到 pCloud 的家目錄區(首頁),導致整個 pCloud 首頁大亂。

pCloud App Bug?同步上去都擺在家目錄,家目錄都亂了

我推論比較像 pCloud app 有問題,處理舊的同步規則時,似乎把所有東西都建立在 pCloud 家目錄,而非保有 Cryptomator 目錄結構:

% tree -L 2 My-Resource 
My-Resource 
├── d
│   ├── J2
│   └── XX
├── masterkey.cryptomator
├── masterkey.cryptomator.########.bkup
├── vault.cryptomator
├── vault.cryptomator.########.bkup
└── 重要.rtf

4 directories, 5 files

下一刻移除 pCloud 再重裝也是一樣,永遠都可以看到 pCloud app 很努力的在同步我剛在原處創建的 Cryptomator 加密庫,並且 pCloud 首頁永遠都是雜亂的 XD 

終於下定決心來找找,如何乾淨地移除 pCloud 設定檔,推論有一些 plist 紀錄著東西,也猜想 pCloud app 版本不同後,新舊版資料紀錄錯亂或是做了什麼相容架構,導致最新版刪不了舊版紀錄的資料,但又一直做相容架構,拿舊版的資料出來用。此時在新版 pCloud app 添加指定目錄同步時,會一直彈跳出不能重複設定的資訊(pCloud app UI 介面上可以看到,目前一條規則也沒有):Can not add new sync: Folder already syncing!

來個乾淨的刪除吧!

先把 pCloud app 從應用程式的目錄拖拉丟去垃圾桶:

% open /Applications 

接著檢查其他資料:

% sudo find /Library -iname "*pcloud*" 2>/dev/null
...
/Library/PrivilegedHelperTools/com.pcloud.pcloudfs.Mounter.Helper
/Library/Logs/DiagnosticReports/pCloud Drive_2023-##-##-#######_#####.diag
/Library/LaunchDaemons/com.pcloud.pcloudfs.Mounter.Helper.plist
...

% find ~/Library -iname "*pcloud*" 2>/dev/null
...
/Users/account/Library/WebKit/com.pcloud.pcloud.macos
/Users/account/Library/Preferences/com.pcloud.pcloud.macos.plist
/Users/account/Library/Application Scripts/com.pcloud.pcloud.macos.pCloudFinderExt
/Users/account/Library/HTTPStorages/com.pcloud.pcloud.macos.binarycookies
/Users/account/Library/Containers/com.pcloud.pcloud.macos.pCloudFinderExt
/Users/account/Library/Containers/com.pcloud.pcloud.macos.pCloudFinderExt/Data/Library/Application Scripts/com.pcloud.pcloud.macos.pCloudFinderExt
/Users/account/Library/Caches/com.pcloud.pcloud.macos

偷看 /Users/account/Library/Preferences/com.pcloud.pcloud.macos.plist 內容,他還記錄著 sync 目錄資訊。

最後就人工把以下的資料刪除:

/Library/PrivilegedHelperTools/com.pcloud.pcloudfs.Mounter.Helper
/Library/Logs/DiagnosticReports/pCloud Drive_2023-##-##-#######_#####.diag
/Library/LaunchDaemons/com.pcloud.pcloudfs.Mounter.Helper.plist
/Users/account/Library/WebKit/com.pcloud.pcloud.macos
/Users/account/Library/Application Scripts/com.pcloud.pcloud.macos.pCloudFinderExt
/Users/account/Library/HTTPStorages/com.pcloud.pcloud.macos.binarycookies
/Users/account/Library/Containers/com.pcloud.pcloud.macos.pCloudFinderExt
/Users/account/Library/Caches/com.pcloud.pcloud.macos
/Users/account/Library/Preferences/com.pcloud.pcloud.macos.plist

原本以為這樣就搞定了,殊不知裝完 pCloud app 啟動後,又發現繼續同步資料。最後想起一招:從 活動監視器 那邊看 pCloud app 開啟的檔案清單



就發現有一處是 ~/.pcloud ,把他刪掉後,終於正常了。

2023年3月22日 星期三

Flutter 開發筆記 - 使用 Admob - BannerAds 版型 @ Flutter 3.7.7, Android Studio 2022.1.1 Patch2, macOS 13.2.1




首先,透過 Android Studio 創建一個專案,預設跑 Build -> Flutter -> Build APK 可以正常跑東西:

flutter build apk

💪 Building with sound null safety 💪

Running Gradle task 'assembleRelease'...                           87.4s
✓  Built build/app/outputs/flutter-apk/app-release.apk (16.8MB).
Process finished with exit code 0

但是在 Project 視窗內,對 android 目錄 -> 右鍵 -> Flutter -> Open Android module in Android Studio 則是會看到:

Unsupported Java. 
Your build is currently configured to use Java 19.0.2 and Gradle 7.5.

Possible solution:
 - Open Gradle wrapper settings, change `distributionUrl` property to use compatible Gradle version and reload the project

解法就是 File -> Project structure -> Project

Android Gradle Plugin Version: 7.4.2
Gradle Version: 7.6.1

就可以很正常搞定了。

接著處理添加 Admob 的用法,參考 developers.google.com/admob/flutter/quick-start 流程:
  1. 先到原本的 flutter 專案上,選擇 terminal -> flutter pub add google_mobile_ads ,運行一陣子後,回去看 pubspec.yaml 可以看到添加了 google_mobile_ads: ^2.3.0 項目
  2. 對 flutter 專案的 Project 視窗內的 android 目錄 -> 右鍵 -> Flutter -> Open Android module in Android Studio ,進入開發單純的 Android 專案一樣,要進行 AndroidManifest.xml 設定,主要是添加 Admob 使用的資訊

<manifest>
    <application>
        <!-- Sample AdMob app ID: ca-app-pub-3940256099942544~3347511713 -->
        <meta-data
            android:name="com.google.android.gms.ads.APPLICATION_ID"
            android:value="ca-app-pub-3940256099942544~3347511713"/>
    <application>
<manifest>

接著用 Build -> Rebuild Project 會看到數個錯誤訊息:
  • Cannot fit requested classes in a single dex file (# methods: 68231 > 65536) 
  • Error: uses-sdk:minSdkVersion 16 cannot be smaller than version 19 declared in library [:google_mobile_ads]
解法就是在 build.gradle (Module: app) 更新:

defaultConfig {
    // ...

    minSdk 19
    multiDexEnabled true

    // ...
}

// ...

dependencies {
    // ...

    implementation "androidx.multidex:multidex:2.0.1"

    // ...
}

接著就是 Sync 以及 Build -> Rebuild Project ,更多資訊須參考 developer.android.com/studio/build/multidex?hl=zh-tw

3. 回到 flutter 專案上,編輯 lib/main.dart

import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  MobileAds.instance.initialize();
  runApp(const MyApp());
}

// ...

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  final BannerAd myBanner = BannerAd(
    // https://developers.google.com/admob/android/test-ads?hl=zh-cn#sample_ad_units
    // https://developers.google.com/admob/ios/test-ads?hl=zh-cn#demo_ad_units
    adUnitId: Platform.isAndroid ? 'ca-app-pub-3940256099942544/6300978111' : 'ca-app-pub-3940256099942544/2934735716' ,
    size: AdSize.banner,
    request: const AdRequest(),
    listener: BannerAdListener(
      // Called when an ad is successfully received.
      onAdLoaded: (Ad ad) => print('Ad loaded.'),
      // Called when an ad request failed.
      onAdFailedToLoad: (Ad ad, LoadAdError error) {
        // Dispose the ad here to free resources.
        ad.dispose();
        print('Ad failed to load: $error');
      },
      // Called when an ad opens an overlay that covers the screen.
      onAdOpened: (Ad ad) => print('Ad opened.'),
      // Called when an ad removes an overlay that covers the screen.
      onAdClosed: (Ad ad) => print('Ad closed.'),
      // Called when an impression occurs on the ad.
      onAdImpression: (Ad ad) => print('Ad impression.'),
    ),
  );

  @override
  void initState() {
    super.initState();
    myBanner.load();
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(

        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Container(
              alignment: Alignment.center,
              width: myBanner.size.width.toDouble(),
              height: myBanner.size.height.toDouble(),
              child: AdWidget(ad: myBanner),
            ),
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

更多資訊: developers.google.com/admob/flutter/banner?hl=zh-cn (英文版顯示 404 NOT Found)

以上就順玩流程,但仍有很多細節未處理,像是廣告沒 load 到,或是 App 或 View/ViewController 切換時廣告資源釋放等等。

2023年3月15日 星期三

Flutter 開發筆記 - 撰寫平台相依性功能,以取得 Android Device 資料為例


研究了一下 Flutter 如何寫平台相依性的程式碼,看一眼也是常見的 Channel or Message 等溝通機制,也滿直觀的。以 Android 平台和 Kotlin 為例,先找到 MainActivity.kt ,接著,在 Code -> Override Methods 可以找到 configureFlutterEngine 可以添加

class MainActivity: FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
    }
}

接著就可以在透過 MethodChannel 建立綁定溝通管道:

import android.os.Build
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
    private val myCHANNEL = "samples.flutter.dev/helper"
    
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, myCHANNEL).setMethodCallHandler {
                call, result ->
            // This method is invoked on the main thread.
            // TODO

            if (call.method == "getBatteryLevel") {
                val batteryLevel = 0; // getBatteryLevel()
                if (batteryLevel != -1) {
                    result.success(batteryLevel)
                } else {
                    result.error("UNAVAILABLE", "Battery level not available.", null)
                }
            } else if (call.method == "getDeviceInfo") {
                result.success(getDeviceInfo())
            } else {
                result.notImplemented()
            }
        }
    }

    private fun getDeviceInfo(): HashMap<String, String> {
        val deviceInfo:HashMap<String, String> = HashMap<String, String>()
        deviceInfo.run {
            put("MODEL", Build.MODEL)
            put("MANUFACTURER", Build.MANUFACTURER)
        }
        return deviceInfo
    }
}

如此在 Flutter Dart 端,就可以呼叫:

class _MyHomePageState extends State<MyHomePage> {
  static const platform = MethodChannel('samples.flutter.dev/helper');

  String _deviceInfo = "system info unknown";
  Future<void> _getDeviceInfo() async {
    Map<String, String> deviceInfo = {};
    try {
      final Map<Object?, Object?> result = await platform.invokeMethod('getDeviceInfo');
      deviceInfo.clear();
      result.forEach((key, value) {
        if (key.runtimeType == String && value.runtimeType == String) {
          deviceInfo[key.toString()] = value.toString();
        }
      });
    } on PlatformException catch (e) {
      deviceInfo.clear();
    } on Exception catch (e) {
      //print("Exception catch: $e ");
    }

    setState(() {
      _deviceInfo = json.encode(deviceInfo);
    });
  }
...

後續就只是完善細節,強烈建議直接觀看官方文件,有完整的細流程資訊和各平台的範例: docs.flutter.dev/development/platform-integration/platform-channels 

Flutter 開發筆記 - 排除 Could not open settings generic class cache for settings file @ macOS 13.2.1, openjdk 19.0.2, Android Studio Electric Eel | 2022.1.1 Patch 2


最近工作上關係,來回顧一下 Flutter app 的開發,預計寫一款非常簡單的 App 試試水溫。結果在 macOS 13.2.1, openjdk 19.0.2, Android Studio Electric Eel | 2022.1.1 Patch 2 環境,預設弄個專案出來卻不能編譯:

Launching lib/main.dart on sdk gphone64 arm64 in debug mode...
Running Gradle task 'assembleDebug'...

FAILURE: Build failed with an exception.

* What went wrong:
Could not open settings generic class cache for settings file '/Users/UserID/AndroidStudio/usb_displayport_helper/android/settings.gradle' (/Users/UserID/.gradle/caches/7.5/scripts/XXXXXX).
> BUG! exception in phase 'semantic analysis' in source unit '_BuildScript_' Unsupported class file major version 63

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 24s
Exception: Gradle task assembleDebug failed with exit code 1

研究了一下,主因是 JDK 與 Gradle 版本問題,需要多做一些事就能排除了。記錄一下。

此時環境:

% java --version
openjdk 19.0.2 2023-01-17
OpenJDK Runtime Environment (build 19.0.2+7-44)
OpenJDK 64-Bit Server VM (build 19.0.2+7-44, mixed mode, sharing)

這時,在既有專案的左邊上方的 Project 小視窗,挑選 android 目錄,點選右鍵 -> Flutter -> Open Android module in Android Studio -> New Window ,在新的 Android Studio 還沒按 Build 就看到訊息:

Unsupported Java. 
Your build is currently configured to use Java 19.0.2 and Gradle 7.5.

Possible solution:
 - Open Gradle wrapper settings, change `distributionUrl` property to use compatible Gradle version and reload the project

這時就來更換預設的環境: File -> Project Structure -> Project

- Android Gradle Plugin Version: 7.4.2
- Gradle Version: 7.6.1

按下 OK 就會開始抓資料,最後也順利編譯後在模擬器上跑了。