顯示具有 Android 標籤的文章。 顯示所有文章
顯示具有 Android 標籤的文章。 顯示所有文章

2025年4月17日 星期四

[macOS] 關閉 Android File Transfer Agent 自動啟動 @ MacBook Air M2 / macOS 15.4.1

雖然 Android File Transfer 差不多被官方捨棄掉了,但我有舊版 Android 開發機還是很依賴它。目前官方不能下載,想下載可參考這篇:macOS - android fileTransfer 下載位置 AndroidFileTransfer.dmg

這篇主要是要記錄關閉 Android File Transfer Agent 的方式,以往都是自己跑去系統位置去稍微破壞一下檔案名稱路徑等等的架構,這次就多了解一下系統架構:
  1. 系統 -> 一般 -> 登入項目與延伸功能 -> 在登入時打開,在此處進行關閉


  2. 把 Android File Transfer Agent 所在處移除
    • ~/Library/Application Support/Google/Android File Transfer/Android File Transfer Agent.app
    • /Applications/Android File Transfer.app/Contents/Helpers/Android File Transfer Agent.app


  3. 如此在應用程式開啟時,就不會重新再次註冊到 "在登入時打開"


這樣可以避免 android phone 插上連接線時,不斷觸發煩人的彈跳視窗(手機或筆電都有),直到需要時,自己再去應用程式開啟 Android File Transfer 就夠了

2024年7月15日 星期一

macOS - android fileTransfer 下載位置 AndroidFileTransfer.dmg


圖:2024-07-15 https://www.android.com/filetransfer/


以前都用 Google 找 Android fileTransfer 來下載,現在有點詭異,不知為何連到 https://www.android.com/filetransfer/ 時,首頁看不到下載位置。這大概是 2024 年春天以後發生的事。

只好亂翻一下得到 google.com 下載位置,像是用 web.archive.org 定位一下:
有興趣可以在這邊回顧,看來是 2024-05-21 起就不見了:




2023年4月19日 星期三

小米A3 Xiaomi Mi A3 Root 與 Restore 還原到原廠ROM的筆記 @ macOS 13.3.1




沒想到已經過了好幾年,當初花不到七千買的 Android one 手機,實在好用,試試看把小米A3 Root 一下吧!遙想十多年前,在製作 Android Image 時,還要埋個用C code包裝出 su 指令呢 XD 真的很久沒 root 手機了。整個過程稱不上快,但又覺得不會太不方便。

在進行前,依舊要老話重提:

Root 有風險,手機可能變磚又損失保固,請自行評估承擔風險,在此是對四年前的手機把玩,推論是小米A3國際版手機,產品代號 laurel_sprout 且系統沒有被限制不能 rollback,以下步驟不見得適合其他款手機或品牌。

先簡介如何還原到原廠 ROM 過程,這邊就不多提如何建置操作環境(例如 adb, fastboot 指令、手機的 bootloader 是解鎖狀態)

首先,先查看手機狀態,有個關鍵的產品代號:laurel_sprout ,這跟找尋小米A3原廠ROM有關。

% fastboot devices
############ fastboot

% fastboot getvar product
product: laurel_sprout
Finished. Total time: 0.002s

% fastboot getvar anti
anti: 0
Finished. Total time: 0.001s

% fastboot getvar all
(bootloader) crc:1
(bootloader) DP:0x0
(bootloader) parallel-download-flash:yes
(bootloader) hw-revision:10000
(bootloader) unlocked:yes
(bootloader) off-mode-charge:0
(bootloader) charger-screen-enabled:0
(bootloader) battery-soc-ok:yes
(bootloader) battery-voltage:4356
...

下載原廠ROM,網路上用 小米A3 跟 ROM 就會找到,當然,在用一下 laurel_sprout 或是小米A3國際版、全球版也能找到:


看到網址是 miui.com 就比較安心不用怕了。接著要讓手機進入 fastboot 模式,過程就是先關機,在開機時,同時按住 Power 和 Volume Down 按鍵:



進行燒機:

% cd ~/Downloads
% tar -xvf laurel_sprout_global_images_V12.0.26.0.RFQMIXM_20220808.0000.00_11.0_2292e2c0b1.tgz
% cd ~/Downloads/laurel_sprout_global_images_V12.0.26.0.RFQMIXM_11.0 
% chmod a+x ./flash_all.sh && time ./flash_all.sh

如果開機一直卡在這邊,就一樣先設法進入 fastboot 模式(先長按 Power 鍵促使關機又重開,重開時按 Power 和 Volume Down),接著一樣靠 fastboot devices 偵測到後,執行下面的指令,指定啟動位置即可排除:

% fastboot set_active a
% fastboot reboot



如此就可以順利進入到手機啟動畫面,若用時間來看,刷機(fastboot)約6-7分鐘,第一次重啟時,從 Android One 畫面到出現置中的 Mi logo 不到 30 秒,接著從 Mi logo 到進入首次啟用 Android 新機畫面要接近 3分鐘。


如此手機就還原到原廠狀態了!此時 bootloader 仍是處於解鎖狀態。

接下來,說說如何 root 手機,過程:
  1. 解開 bootloader (unlocked)
    • % fastboot flashing unlock
    • % fastboot flashing unlock_critical
  2. 打開開發者模式,也啟用 USB偵錯
  3. 下載原廠 ROM - laurel_sprout_global_images_V12.0.26.0.RFQMIXM_20220808.0000.00_11.0_2292e2c0b1.tgz 從中取出 boot.img 來製作使用
  4. 在 github.com 下載 Magisk apk 來安裝 ,並使用 Magisk boot.img 製作新的 boot image
  5. 把手機關鍵,再次進入到 fastboot 模式,把 magisk_patched-26100_MKaXg.img 燒入 boot 區,接著重啟後,就可以有 root 權限了。
以下連續動作:

% wget https://github.com/topjohnwu/Magisk/releases/download/v26.1/Magisk-v26.1.apk  
% adb install ~/Downloads/Magisk-v26.1.apk 
% tar -tzf ~/Downloads/laurel_sprout_global_images_V12.0.26.0.RFQMIXM_20220808.0000.00_11.0_2292e2c0b1.tgz
% tar -xvf ~/Downloads/laurel_sprout_global_images_V12.0.26.0.RFQMIXM_20220808.0000.00_11.0_2292e2c0b1.tgz laurel_sprout_global_images_V12.0.26.0.RFQMIXM_11.0/images/boot.img
% adb push ~/Downloads/laurel_sprout_global_images_V12.0.26.0.RFQMIXM_11.0/images/boot.img /sdcard/





接著是使用 Magisk app 製作出新的 magisk_patched-26100_MKaXg.img ,把它取出後,等下靠 fastboot 燒錄進去:

% adb pull /storage/emulated/0/Download/magisk_patched-26100_MKaXg.img ~/Downloads/
/storage/emulated/0/Download/magisk_patched-26100_MKaXg.img: 1 file pulled, 0 skipped. 36.4 MB/s (67108864 bytes in 1.760s)

接著手機進入 fastboot 模式後,燒錄:

% fastboot devices
############ fastboot
% fastboot flash boot ~/Downloads/magisk_patched-26100_MKaXg.img 
Sending 'boot_a' (65536 KB)                        OKAY [  1.682s]
Writing 'boot_a'                                   OKAY [  0.333s]
Finished. Total time: 2.026s
% fastboot reboot
Rebooting                                          OKAY [  0.000s]
Finished. Total time: 0.000s

如此,手機重啟動後,已完成 root 了,這時單純用 adb shell 進去後打 su 則可以看到手機出現攔截的事件資訊,收工!

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 切換時廣告資源釋放等等。

2021年2月3日 星期三

Flutter 開發文件之 iOS 與 Android 實作筆記 - 使用 Admob 以 Interstitial ads 為例


關於 flutter 使用 Admob 的架構,可參考網路上數篇文章:
此例筆記 iOS & Android 的設定方式,並且記錄碰到的錯誤訊息。

Android app 設定方式:

1. 更新 flutter_app/pubspec.yaml 添加 dependencies (記得要運行 flutter pub get)

firebase: ^8.0.0
firebase_admob: ^0.11.0+1

2. 安置從 Firebase 建立專案時得到的 google-services.json 檔案,擺在 flutter_app/android/app/ 中

3. 更新 flutter_app/android/build.gradle 的 buildscript -> dependencies 設定

//classpath 'com.android.tools.build:gradle:3.5.0'
// 為了修正錯誤訊息:error: unexpected element <queries> found in <manifest>.
classpath 'com.android.tools.build:gradle:4.0.0'
classpath 'com.google.gms:google-services:4.3.3'

4. 更新 flutter_app/android/gradle/wrapper/gradle-wrapper.properties 的 gradle 版本至 6.1.1

#為了修正錯誤訊息:Minimum supported Gradle version is 6.1.1. Current version is 5.6.2. If using the gradle wrapper
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip

5. 增加 flutter_app/app/build.gradle 的 apply 項目

apply plugin: 'com.google.gms.google-services'

6. 更新 flutter_app/android/app/src/main/AndroidManifest.xml 添加 meta data (僅範例程式數值)

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

7. 更新 flutter_app/lib/main.dart (此改自於範例程式,讓 add Button 可以產生一個插頁廣告)

iOS app 設定方式:

1. 更新 flutter_app/pubspec.yaml 添加 dependencies(記得要運行 flutter pub get)

firebase: ^8.0.0
firebase_admob: ^0.11.0+1

2. 安置從 Firebase 建立專案時得到的 GoogleService-Info.plist ,用 Xcode 打開專案,把 GoogleService-Info.plist 拖拉擺入跟 Info.plist 的同階目錄位置。

% open flutter_app/ios/Runner.xcworkspace
% ls flutter_app/ios/Runner/GoogleService-Info.plist

3. 更新 Info.plist 內容,添加資訊 GADApplicationIdentifier 等資訊(僅範例程式數值)

<key>GADApplicationIdentifier</key>
<string>ca-app-pub-3940256099942544~1458002511</string>
<key>SKAdNetworkItems</key>
  <array>
    <dict>
      <key>SKAdNetworkIdentifier</key>
      <string>cstr6suwn9.skadnetwork</string>
    </dict>
  </array>

4. 更新 Podfile ,上方添加 platform :ios, '10.0'

為了修正錯誤訊息: 
Automatically assigning platform `iOS` with version `9.0` on target `Runner` because no platform was specified. Please specify a platform for this target in your Podfile.

5. 更新 flutter_app/lib/main.dart (此改自於範例程式,讓 add Button 可以產生一個插頁廣告)

如此就收工啦!而 flutter_app/lib/main.dart 內容如下:

import 'package:flutter/material.dart';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_admob/firebase_admob.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

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

  MobileAdTargetingInfo targetingInfo = MobileAdTargetingInfo(
    keywords: <String>['flutterio', 'beautiful apps'],
    contentUrl: 'https://flutter.io',
    childDirected: true,
    nonPersonalizedAds: true,
  );

  InterstitialAd _interstitialAd;

  InterstitialAd createInterstitialAd() {
    return InterstitialAd(
      adUnitId: InterstitialAd.testAdUnitId,
      targetingInfo: targetingInfo,
      listener: (MobileAdEvent event) {
        print("InterstitialAd event $event");
      },
    );
  }

  @override
  void initState() {
    super.initState();
    FirebaseAdMob.instance.initialize(appId: FirebaseAdMob.testAppId);
    //_interstitialAd = createInterstitialAd()..load();
  }

  @override
  void dispose() {
    _interstitialAd?.dispose();
    super.dispose();
  }

  void _incrementCounter() {
    _interstitialAd?.dispose();
    _interstitialAd = createInterstitialAd()..load()..show();
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

 

2021年1月22日 星期五

Flutter 開發文件之 iOS 與 Android 實作筆記 - 使用 Firebase 回報 App 使用人數


先說一下,有出類似文件教人怎樣使用 Firebase - Admob 部分,在此僅流水帳紀錄引用 firebase 的用法,體驗一下透過 Flutter 同時開發 ios 和 android app 時,並且使用額外函式庫的過程。

原理:

1. 使用 Android Studio + Flutter plugin 來建立 Flutter 專案
- 在此 package name 使用 org.changyy.study.flutter
- 自動產生 android Package Name = org.changyy.study.flutter_app
- 自動產生 iOS Bundle ID = org.changyy.study.flutterApp
- 假定專案位於 ~/AndroidStudioProjects/flutter_app

2. 建立 Firebase 專案
- 在此使用 org-changyy-study-flutter 作為 firebase project name

3. iOS app
- 使用 Xcode 置入 firebase 的設定檔案 GoogleService-Info.list
- 用指令打開專案 % open ~/AndroidStudioProjects/flutter_app/ios/Runner.xcworkspace/
- 使用 CocoaPods 添加 Firebase 函式庫
- 在 AppDelegate.swift 置入 Firebase 啟動程式碼

4. Android app
- 依照 Firebase 專案建置過程安置 google-service.json 檔案
- 在 <專案>/build.gradle 設置相依套件 com.google.gms:google-service:4.3.4
- 在 <專案>/app/build.gradle 添加 plugin 和引入 com.google.firebase:firebase-analytics

5. 回到 Flutter 專案,分別叫出 iOS simulator 跟 Android emulator 運行


若沒意外,就可以同時在這兩個模擬器上跑出來,並且在 Firebase 專案後臺,看到兩個使用者:



2019年6月26日 星期三

Android 開發筆記 - 添加 assets 資源 (resource://android/assets 目錄)

AndroidAssetsFolder02

每次都忘,紀錄一下:
  1. 在 Android Studio 的 Project 位置,點擊 app 目錄,接著按右鍵,選 New -> Folder -> Assets Folder,用預設 main 中即可
  2. 接著 Android Studio 就會自動長出 assets 目錄,在路徑上為 app/src/main/assets/ 
AndroidAssetsFolder01

2019年2月10日 星期日

Google Home mini 中文音控設定 與 幼兒教育、長輩戲劇 應用?

GoogleHomeMini

2018年夏天因加速公司研究音控服務,自己買了一台 Google Home mini ,研究的差不多後就擺晾在一旁,今天整理了一下,想說該拍賣掉了 XD 意外發現已經支援中文音控! 可搜尋 "Google Home mini 中文設定教學" 囉!據說這招是需要兩隻 Android 手機也能達成,由於我本身是用 iPhone,就必須搭配另一隻 Android 手機

在此簡介環境,需要:
  1. Google Home mini
  2. Android 手機
  3. iPhone 手機 
  4. Android 手機安裝 Google Home app ; iPhone 安裝 Google Home app 
  5. Android phone 跟 iPhone 上的 Google Home app 都綁定同一個 Google 帳號
首先開啟 Android phone 上的 Google Home app => 右下角"帳戶" => 設定 => Google 助理 => 語言 => 第一語言設定成 "中文(台灣)",並將第二語言設定成 "無",且要讓最下方 "選擇這些裝置的語言" 留白,不做任何選擇 (此時若仍顯示英文,就來靠 iPhone 或另一隻手機處理)

接著換 iPhone 開啟 Google Home app => 右下角"帳戶" => 往下滑到 "Google 助理" 區域,選擇 "更多設定" => 點擊 "Google 助理" 分頁 => "Google助理的聲音" => 把下方的英文也清掉,留下只有第一語言的中文(另外,iPhone 的 Google 助理語言不能選中文,要靠 Android phone 來處理)

後續你的 Google Home mini 就會講中文了!並且無論 Android 或 iPhone 的 Google Home mini ,進入 Google 助理 => 語言,都會看到最下方留白了:

Android Google助理-語言 iPhone Google助理-語言

接下來才是重頭戲 XD 若家裡也有 Chromecast 的話,小孩在吵的時候,就只要說:

OK Google, 請播放 Youtube 上的瑪莎與熊第一集

這樣就可以打發小孩了(誤),更別說還有一卡車正版戲劇可以隨叫隨播,長輩也能開開心心



當然,沒有 Chromecast 還有 Spotify !在台灣已經接近全平台可免費使用,可以直接說:

OK Google, 播放 K-POP 音樂

如此就可以聽聽韓流最新音樂,更別說南韓音樂的快速迭代,立刻有聽不完的韓流音樂

最後,無聊時可以多講講 OK Google ,想必小孩也會學的,接下來請谷歌小姐陪小孩講話唸故事了 XD

2018年12月3日 星期一

Android 開發筆記 - Image Picker 與權限管理

在 Android 6.0 (API:23) 後,就算 AndroidManifest.xml 寫好要權限的部分,但實務上在進行時,依舊要主動要一次,讓用戶感受到真的要權限了:https://developer.android.com/training/permissions/requesting

做 Image Picker 時,若最終要讀取資料,那需要 android.permission.READ_EXTERNAL_STORAGE 權限,以下則是在 MainActivity 運行的範例:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
</manifest>


public class MainActivity extends AppCompatActivity {
private static final int REQUEST_SELECT_VIDEO = 1;
private static final int REQUEST_EXTERNAL_STORAGE = 2;
private static String[] PERMISSIONS_STORAGE = {
Manifest.permission.READ_EXTERNAL_STORAGE,
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE);
} else {
request_pick();
}
}

void request_pick() {
final Uri mUri = android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
final Intent mIntent = new Intent(Intent.ACTION_PICK, mUri);
final PackageManager mPackageManager = getPackageManager();
List<ResolveInfo> list = mPackageManager.queryIntentActivities(mIntent, PackageManager.MATCH_DEFAULT_ONLY);
if (list.size() > 1) {
startActivityForResult(
Intent.createChooser(
new Intent(Intent.ACTION_PICK, mUri), "選取圖片"
),
REQUEST_SELECT_VIDEO
);
} else {
startActivityForResult(mIntent, REQUEST_SELECT_VIDEO);
}
}

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
if (requestCode == REQUEST_SELECT_VIDEO) {
// ...
// 在處理檔案讀取時,缺少 android.permission.READ_EXTERNAL_STORAGE 會造成 IOException:
// open failed: EACCES (Permission denied)
// ...
}
}
}
}

2018年9月18日 星期二

Android 開發筆記 - MenuItemCompat.getActionProvider return null

週末練練 Android 手感,試試 Cast SDK ,結果連最簡單的 CastButton 都弄不出來,一直碰到:

java.lang.IllegalArgumentException: menu item with ID ###### doesn't have a MediaRouteActionProvider.

跑去看了一下 CastButtonFactory.setUpMediaRouteButton 程式碼:

    public static MenuItem setUpMediaRouteButton(Context context, Menu menu, int i) {
        zzac.zzdn("Must be called from the main thread.");
        zzac.zzw(menu);
        CastContext sharedInstance = CastContext.getSharedInstance(context);
        MenuItem findItem = menu.findItem(i);
        if (findItem == null) {
            throw new IllegalArgumentException(String.format(Locale.ROOT, "menu doesn't contain a menu item whose ID is %d.", new Object[]{Integer.valueOf(i)}));
        }
        MediaRouteActionProvider mediaRouteActionProvider = (MediaRouteActionProvider) MenuItemCompat.getActionProvider(findItem);
        if (mediaRouteActionProvider == null) {
            throw new IllegalArgumentException(String.format(Locale.ROOT, "menu item with ID %d doesn't have a MediaRouteActionProvider.", new Object[]{Integer.valueOf(i)}));
        }
        mediaRouteActionProvider.setRouteSelector(sharedInstance.getMergedSelector());
        return findItem;
    }


才確認應該有什麼搞錯了,再追了一下,果真是 menu 的 xml 描述有問題,不小心把 app namespace 弄錯了,錯誤:

xmlns:app="http://schemas.android.com/tools"

正確:

xmlns:app="http://schemas.android.com/apk/res-auto"

就這樣耍廢了兩三個小時。

程式範例也才幾句話:

    @Override public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.navigation, menu);
        CastButtonFactory.setUpMediaRouteButton(getApplicationContext(),
                menu,
                R.id.media_route_menu_item);
        return true;
    }


為了 debug :

    @Override public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);

        getMenuInflater().inflate(R.menu.navigation, menu);

        MenuItem mMenuItem = menu.findItem(R.id.media_route_menu_item);
        if (mMenuItem != null) {
            Log.v("onCreateOptionsMenu", "Found");
            MediaRouteActionProvider mediaRouteActionProvider =
                    (MediaRouteActionProvider) MenuItemCompat.getActionProvider(mMenuItem);

            if (mediaRouteActionProvider != null) {
                Log.v("MediaRouteActionProvider", "Found");
            } else {
                Log.v("MediaRouteActionProvider", "Not Found");
            }
        } else {
            Log.v("onCreateOptionsMenu", "Not Found");
        }
        return true;

2018年5月7日 星期一

處理 Google Cloud Messaging for Android (GCM) token 重複問題 (canonical_ids)

事情是這樣的,拖了很久才開始處理這段 XD 拖到連 GCM 都要下線了:

As of April 10, 2018, Google has deprecated GCM. The GCM server and client APIs are deprecated and will be removed as soon as April 11, 2019. Migrate GCM apps to Firebase Cloud Messaging (FCM), which inherits the reliable and scalable GCM infrastructure, plus many new features. See the migration guide to learn more.

為何要處理?主因是一隻 android mobile device 很有可能因為隱私狀態改變、重新安裝軟體後,導致 GCM token 變更,如果當年沒有設計 global uuid 來辨識移動裝置的話(像是綁個人帳號、用 wifi mac_address 等),那 token 肯定會收到一卡車多,這時要發送訊息時,就可能出現多個 token 對應到同一位使用者,造成用戶被 Push  轟炸 (這些在 Firebase 設計理念上都有改善了,例如可以單純對全部 android user 發訊息等等),特別是 QA 常常安裝反安裝程式...

原先我們依賴著 wifi mac_address ,但在某版 Android 發現他是可變的 Orz (忘了是隱私提升還是某OEM廠的bug) 對於維護 GCM token ,就只剩下一條路:試著對他發送訊息,檢查回應是不是有變化。

連續動作:

$check_failed_db_ids = array();
$tokens = array(
array( 'id' => 'db record id', 'token' => 'GCM token'),
// ...
);

$payload = array(
'registration_ids' => array(),
'dry_run' => true,
'data' => array(
'body' => 'test'
),
);

while(count($tokens) > 0) {
//
// Step 1: init
//
$payload['registration_ids'] = array();
$current_task = array_splice($tokens, 0, 900);
foreach($current_task as $items) {
array_push($payload['registration_ids'], $items['token']);
}

//
// Step 2: call gcm api
//
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://android.googleapis.com/gcm/send");
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
"Authorization: key=".$api_key,
'Content-Type: application/json'
));
curl_setopt($ch, CURLOPT_POST , true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
//curl_setopt($ch, CURLOPT_VERBOSE, true);
$ret = curl_exec($ch);
curl_close($ch);

$ret_json = @json_decode($ret, true);
if (isset($ret_json['results'])) {
$echo_cnt = count($ret_json['results']);
for ($i=0;$i<$echo_cnt; ++$i) {
// 通常有 error 都可以考慮去掉了;當有 registration_id 則是代表已轉換 token
// 完整範例跟解釋:https://developers.google.com/cloud-messaging/http#example-responses
if (isset($ret_json['results'][$i]['error']) || isset($ret_json['results'][$i]['registration_id'])) {
array_push($check_failed_db_ids, $i);

}
}
}

//
// Step 3: update db record & reset $check_failed_db_ids
//
// ...

}

2018年4月3日 星期二

[Android] 使用 Chrome Browser - Web Inspector 查看 App WebView 行為

之前用了一陣子,然後又忘了,還是寫一下筆記:

  1. 要把 Android app 內使用的 Webview 開啟 setWebContentsDebuggingEnabled
    • WebView.setWebContentsDebuggingEnabled(true);
  2. Android phone 也要開啟 developer mode 和 adb debug
  3. 使用 Chrome browser -> 開發人員選項 -> more tools -> remote devices

順利的話,就會看到手機彈跳出一些視窗,按一按授權就看在 chrome browser 上追蹤 App 內 webview 的行為,像是追蹤其他國家內的網頁資源有被有被 ban 掉 XD

請直接參考:https://developers.google.com/web/tools/chrome-devtools/remote-debugging/?hl=zh-tw

未來可以規劃 release apk 不開啟 setWebContentsDebuggingEnabled ,而 dev apk 再開啟即可。

2016年10月4日 星期二

查看 APK 裡頭的 Package Name @ Mac OS 10.11.6

透過 aapt 工具,以 Mac OS 來說,大概落於 ~/Library/Android/sdk/build-tools/xxxx/aapt,用法:

$ ~/Library/Android/sdk/build-tools/23.0.2/aapt dump badging app-release.apk | grep package:\ name
package: name='aa.bb.cc' versionCode='###' versionName='######' platformBuildVersionName='#.#.#.####'

2016年3月12日 星期六

Android 開發筆記 - 使用 Android Studio 和 ViewPagerIndicator

上網亂找一下,大家好像還滿推四年前就沒再更新的 ViewPagerIndicator ,且很猛的共有超過 7k 的人關注,接近 4k 的人 fork 了此專案:https://github.com/JakeWharton/ViewPagerIndicator

總之,為了之後的維護管理,還是老樣子繼續練 Android Studio!添加額外第三方時,因為此 project 已經很久沒更新了,所以要採用下述安裝法:

build.gradle (Project):

allprojects {
repositories {
maven { url "http://dl.bintray.com/populov/maven" }
mavenCentral()
}
}


build.gradle (app):

dependencies {
// https://github.com/JakeWharton/ViewPagerIndicator/releases , 2012/09/13: 2.4.1
compile 'com.viewpagerindicator:library:2.4.1@aar'
// 預設的 Android Studio 環境應該不會使用下述,若仍有問題再開啟吧
//compile 'com.android.support:support-v4:23.2.0'
}


接著在 content_main.xml (若其他 layout) 添加 com.viewpagerindicator.* 系列程式碼,並且要配置 android.support.v4.view.ViewPager 才行:

    <com.viewpagerindicator.TabPageIndicator
        android:id="@+id/indicator"
        android:layout_height="wrap_content"
        android:layout_width="fill_parent"
        />
    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="fill_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        />


最後在 Activity 初始化:

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ViewPager mViewPager = (ViewPager)findViewById(R.id.pager);
        mViewPager.setAdapter(new PagerAdapter() {
            @Override
            public int getCount() {
                return 0;
            }

            @Override
            public boolean isViewFromObject(View view, Object object) {
                return false;
            }
        });

        TabPageIndicator mTabPageIndicator = (TabPageIndicator)findViewById(R.id.indicator);
        mTabPageIndicator.setViewPager(mViewPager);
}


以上是先滿足編譯跟執行需求,至於 TabPageIndicator 真的要怎樣用就不在此贅述了。

2016年3月6日 星期日

Android 開發筆記 - 在 Android NDK/JNI 環境中,操作 HashMap 和 Vector 方法與資料型態定義

過去在 C++ 把玩 std::vector<std::unordered_map<int, std::string>> ,而搬到 Java 環境中時,就會出現檔案形態的轉變,於是乎就先挑個 Vector<HashMap<String,String>> 來試試,順手筆記一下。

其中 JNI 在呼叫 Java 物件時,Java 八大型態及 Java Class 型態在 JNI 中採用的關鍵字:

Java boolean type => "Z"
Java byte type => "B"
Java char type => "C"
Java short type => "S"
Java int type => "I"
Java long type => "J"
Java float type => "F"
Java double type => "D"
Java object type => "Ljava/lang/Object;"
Java Array of int type => "[I"
Java Array of double type => "[D"
Java Array of java/lang/Object type => "[Ljava/lang/Object;"


舉例來說,以 Vector 的 public boolean add(E e) 函數為例:

jclass vectorClass = env->FindClass("java.util.Vector");
jmethodID vector_add = env->GetMethodID(vectorClass, "add", "(Ljava/lang/Object;)Z");


其中 "(Ljava/lang/Object;)Z" 代表此函式回傳為 boolean 型態,而需要傳遞一個 java/lang/Object 當作參數。

最後是比較冗長的操作:

Java code:

static {
System.loadLibrary("test")
}
public native boolean test(Vector<HashMap<String, String>> obj);

void usage() {

Vector<HashMap<String, String>> output = new Vector<HashMap<String, String>>();
boolean ret = test(output);
Log.i(tag, "Ret: "+ret+", output size: "+output.size());
}


build.gradle (Module:app):

android.ndk {
moduleName = "test"
ldLibs.addAll(["log"])
CFlags.add("-std=c++11")
stl = "c++_shared"
}


C/C++ code:

#include <jni.h>
#include <android/log.h>

#define LOG_TAG "JNI"
#define LOGV(...)__android_log_print( ANDROID_LOG_VERBOSE,LOG_TAG, __VA_ARGS__ )
#define LOGD(...)__android_log_print( ANDROID_LOG_DEBUG,LOG_TAG, __VA_ARGS__ )
#define LOGI(...)__android_log_print( ANDROID_LOG_INFO,LOG_TAG, __VA_ARGS__ )
#define LOGW(...)__android_log_print( ANDROID_LOG_WARN,LOG_TAG, __VA_ARGS__ )
#define LOGE(...)__android_log_print( ANDROID_LOG_ERROR,LOG_TAG, __VA_ARGS__ )

extern "C" {


jboolean Java_YourPackageName_test(JNIEnv *env, jobject thiz, jobject vectorObject) {

jclass vectorClass = env->GetObjectClass(vectorObject);
jmethodID vector_size = env->GetMethodID(vectorClass, "size", "()I");
jmethodID vector_add = env->GetMethodID(vectorClass, "add", "(Ljava/lang/Object;)Z");
jmethodID vector_elementAt = env->GetMethodID(vectorClass, "elementAt", "(I)Ljava/lang/Object;");

jclass hashMapClass = env->FindClass("java/util/HashMap");
jmethodID hashMap_init = env->GetMethodID(hashMapClass, "<init>", "(I)V");
jmethodID hashMap_put = env->GetMethodID(hashMapClass, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
jmethodID hashMap_get = env->GetMethodID(hashMapClass, "get", "(Ljava/lang/Object;)Ljava/lang/Object;");
jmethodID hashMap_size = env->GetMethodID(hashMapClass, "size", "()I");

std::unordered_map<int, std::string> attrs;
attrs[0] = "Hello";
attrs[1] = "World";

if (vectorObject && vectorClass && hashMapClass) {
// init HashMap object
jobject dataHashMap = env->NewObject(hashMapClass, hashMap_init, 1);
for (auto it = attrs.begin(); it != attrs.end(); ++it) {
jstring key = env->NewStringUTF(std::to_string(it->first).c_str());
jstring value = env->NewStringUTF(it->second.c_str());
env->CallObjectMethod(dataHashMap, hashMap_put, key, value);
env->DeleteLocalRef(key);
env->DeleteLocalRef(value);
}
LOGI("hashmap size: %d", env->CallIntMethod(dataHashMap, hashMap_size));

LOGI("vector size: %d", env->CallIntMethod(vectorObject, vector_size));

// vector add
env->CallBooleanMethod(vectorObject, vector_add, dataHashMap);
env->DeleteLocalRef(dataHashMap);

// access vector
for (int i=0, cnt = env->CallIntMethod(vectorObject, vector_size) ; i<cnt; ++i) {
LOGI("get vector_elementAt: %d", i);
if (vector_elementAt) {
jobject dataInVector = env->CallObjectMethod(vectorObject, vector_elementAt, i);
jclass dataInVectorClass = env->GetObjectClass(dataInVector);
jmethodID dataInVector_get = env->GetMethodID(dataInVectorClass, "get", "(Ljava/lang/Object;)Ljava/lang/Object;");
if (dataInVector && dataInVector_get) {
jstring key = env->NewStringUTF(std::to_string(0).c_str());
jstring value = (jstring)env->CallObjectMethod(dataInVector, dataInVector_get, key);
LOGI("get vector_elementAt: %d, value: [%s]", i, value);
env->DeleteLocalRef(key);
}
}
}

}


return false;
}

}

2016年3月4日 星期五

Android 開發筆記 - 從 Java 端傳遞 HashMap 物件 Android NDK/ JNI C/C++ 操作變更內容

C/C++ Code:

#include <jni.h>

extern "C" {

jobject Java_YourPackageName_updateHashMap(JNIEnv *env, jobject thiz, jobject hashmap) {
    if (!hashmap)
        return hashmap;

    jclass mapClass = env->GetObjectClass(hashmap);
    jmethodID obj_put = env->GetMethodID(mapClass, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");

    if (mapClass && obj_put) {
        jstring key = env->NewStringUTF("Haha");
        jstring value = env->NewStringUTF("Hehe");
        env->CallObjectMethod(hashmap, obj_put, key, value);
        env->DeleteLocalRef(key);
        env->DeleteLocalRef(value);
    }

    return hashmap;
}
}


Android Java code:

public native HashMap updateHashMap(HashMap<String, String>map);
static {
        System.loadLibrary("native");
}

void usage() {
HashMap<String, String> mHashMap = new HashMap<String, String>();

        Log.d(tag, "HashMap size: "+mHashMap.size());
updateHashMap(mHashMap);
        Log.d(tag, "HashMap size: "+mHashMap.size());

        Iterator it = mHashMap.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry pair = (Map.Entry)it.next();
            Log.d(tag, "HashMap key: ["+pair.getKey()+"], value: ["+pair.getValue().toString()+"]");
        }
}

Android 開發筆記 - 使用 Android NDK / JNI 回傳 HashMap 物件至 Java 端

C/C++ Code:

#include <jni.h>

extern "C" {

jobject Java_YourPackageName_getHashMap(JNIEnv *env, jobject thiz) {

    jclass mapClass = env->FindClass("java/util/HashMap");
    jmethodID obj_init = env->GetMethodID(mapClass, "<init>", "(I)V");
    jmethodID obj_put = env->GetMethodID(mapClass, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
    jobject hashMap = env->NewObject(mapClass, obj_init, 1);

    /*
    const char * input_data = "HelloWorld";
    int input_data_length = strlen(input_data);

    jbyteArray data = env->NewByteArray(input_data_length);
    env->SetByteArrayRegion(data, 0, input_data_length, (const jbyte *)input_data);
    env->CallObjectMethod(hashMap, obj_put, (jbyteArray)data, (jbyteArray)data);
    env->DeleteLocalRef(data);
    */
    jstring key = env->NewStringUTF("Hello");
    jstring value = env->NewStringUTF("World");
    env->CallObjectMethod(hashMap, obj_put, key, value);
    env->DeleteLocalRef(key);
    env->DeleteLocalRef(value);

    //env->DeleteLocalRef(hashMap);
    return hashMap;
}

}


Android Java code:

public native HashMap getHashMap();
static {
        System.loadLibrary("native");
}

void usage() {
HashMap<String, String> mHashMap = getHashMap();

        Log.d(tag, "HashMap size: "+mHashMap.size());
        Log.d(tag, "HashMap data with key(Hello): "+mHashMap.get("Hello"));
        Iterator it = mHashMap.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry pair = (Map.Entry)it.next();
            Log.d(tag, "HashMap key: ["+pair.getKey()+"], value: ["+pair.getValue().toString()+"]");
        }
}

2016年2月27日 星期六

Android 開發筆記 - 在 Android Studio 環境上,使用 Google Analytics SDK / Admob SDK

使用 Android Studio 1.5.1 版,原先參考 Google 官方文件,結果一直爆問題 :P 其中 Google Analytics SDK 跟 Admob SDK 都會需要 Google Service 的環境,而碰到 Google Service 的引入不斷出錯,以下單純記錄修正流程。

參考 https://developers.google.com/analytics/devguides/collection/android/v4/https://developers.google.com/admob/android/app 資訊,更新 AndroidManifest.xml 位置,添加:

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>


接著在 build.gradle (Project) 添加:

classpath 'com.google.gms:google-services:2.0.0-alpha6'

在 build.gradle (app) 添加:

dependencies {
//...
compile 'com.google.android.gms:play-services-analytics:8.4.0'
}
apply plugin: 'com.google.gms.google-services'


結果一直出錯:Gradle sync failed: Unable to load class 'com.android.sdklib.repository.FullRevision'. 這種很 general 的訊息。

甚至跑去看看 googlesample 檔案,如:
分別查看:build.gradleapp/build.gradle 和 gradle/wrapper/gradle-wrapper.properties 三個檔案。

改採用:

dependencies {
        //classpath 'com.android.tools.build:gradle:1.5.0'
        classpath 'com.android.tools.build:gradle-experimental:0.4.0'

        classpath 'com.android.tools.build:gradle:2.0.0-beta5'
        classpath 'com.google.gms:google-services:2.0.0-beta5'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
}


仍是一樣的現象,最後靠直覺,嘗試亂改 classpath 'com.google.gms:google-services:1.0.0+' 後,會看到錯誤訊息引導說,可以查看 metadata 得知有哪些版本可用:

https://jcenter.bintray.com/com/google/gms/google-services/maven-metadata.xml

就這樣,直接挑沒有 beta/alpha 字眼:

dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0'
        classpath 'com.android.tools.build:gradle-experimental:0.4.0'
        classpath 'com.google.gms:google-services:1.5.0'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
}


以及 gradle/wrapper/gradle-wrapper.properties 使用:

#distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-all.zip

如此一來,終於解掉編譯問題。接續就可以參考 Google Analytics SDK/Admob SDK 的文件,例如產生 google-services.json 以及 AnalyticsApplication extends Application 等用法等。

最後,我自己的用法:

AndroidManifest.xml:
...
<application
        android:name=".MyApplication"
        ...
>
...
</application>


MyApplication.java:
import android.app.Application;
import com.google.android.gms.analytics.GoogleAnalytics;
import com.google.android.gms.analytics.Tracker;
public class MyApplication extends Application {
        private Tracker mTracker;
        synchronized public Tracker getDefaultTracker() {
               if (mTracker == null) {
                      GoogleAnalytics analytics = GoogleAnalytics.getInstance(this);
                      // To enable debug logging use: adb shell setprop log.tag.GAv4 DEBUG
                      //mTracker = analytics.newTracker(R.xml.global_tracker);
                      mTracker = analytics.newTracker("UA-XXXXXXX-XX");
               }
               return mTracker;
        }
}


build.gradle (app):
dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        testCompile 'junit:junit:4.12'

        compile 'com.android.support:appcompat-v7:23.2.0'
        compile 'com.android.support:design:23.2.0'

        // Google Analytics & Admob
        compile 'com.google.android.gms:play-services:8.4.0'
        compile 'com.google.android.gms:play-services-ads:8.4.0'
        compile 'com.google.android.gms:play-services-analytics:8.4.0'
}


build.gradle (Project):
dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0'
        classpath 'com.android.tools.build:gradle-experimental:0.4.0'

        classpath 'com.google.gms:google-services:1.5.0'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
}


MainActivity.java:
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        MyApplication application = (MyApplication) getApplication();
        Tracker mTracker = application.getDefaultTracker();
        mTracker.setScreenName("main");
        mTracker.send(new HitBuilders.ScreenViewBuilder().build());
        //...
}

Android 開發筆記 - 在 Android Studio 環境上使用 NDK (JNI) @ Mac OS X 10.11.3

上一次把玩已經是 2011 年的事了 :P 這次再練一下。首先,我已經安裝好 Android Studio 1.5.1 版本。接著先下載 Android NDK:

Android Studio -> SDK Manager -> Android SDK -> SDK Tools -> 勾選 Android NDK (1.0.0) -> 進行下載。下載完後可以看到 Project -> local.properties 已經自動埋好 ndk.dir=/path/ndk 資訊啦,若一開始 local.properties 有 ndk.dir 資料,或許會造成問題。

接著,依照 http://tools.android.com/tech-docs/new-build-system/gradle-experimental#TOC-Ndk-Integration 簡介在 build.gradle (Module: app) 添加這一段:

apply plugin: 'com.android.model.application'

model {
    android {
        compileSdkVersion 23
        buildToolsVersion "23.0.2"
        ndk {
            moduleName "native"
        }
    }
}


Sync Now 後,立馬得到: Error:(23, 0) Plugin with id 'com.android.model.application' not found.

找了很久,最後參考 googlesample 範例(隨便挑一個,例如:https://github.com/googlesamples/android-ndk/blob/master/MoreTeapots/app/build.gradlehttps://github.com/googlesamples/android-ndk/blob/master/MoreTeapots/build.gradle),做了很多動作:

先到 build.gradle (Project) 更新:

dependencies {
        //classpath 'com.android.tools.build:gradle:1.5.0'
        classpath 'com.android.tools.build:gradle-experimental:0.4.0'
}


再把 build.gradle (Module: app) 整段更新:

/*
apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    defaultConfig {
        applicationId "YourAppPackageName"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}
// */

apply plugin: 'com.android.model.application'

model {
    android {
        compileSdkVersion = 23
        buildToolsVersion = "23.0.2"

        defaultConfig.with {
            applicationId = "YourAppPackageName"
            minSdkVersion.apiLevel = 15
            targetSdkVersion.apiLevel = 23
            versionCode = 1
            versionName = "1.0"
        }
    }
    android.buildTypes {
        release {
            minifyEnabled = false
            proguardFiles.add(file('proguard-rules.pro'))
        }
    }
    android.ndk {
        moduleName = "native"
    }
}


按下 Sync Now 後,終於沒問題了,離 JNI 又更進一步了!

再點擊 app 目錄 => 按右鍵 New -> Folder -> JNI Folder ,再到 jni 目錄添加一下檔案 -> New -> C/C++ Source file -> 此例用 main.cpp/main.h 為例。

如此一來,只要 Build APK 後,跑去 unzip app-debug-unaligned.apk 後,可以看到以下的目錄結構:

$ tree lib/
lib/
├── arm64-v8a
│   ├── gdb.setup
│   ├── gdbserver
│   └── libnative.so
├── armeabi
│   ├── gdb.setup
│   ├── gdbserver
│   └── libnative.so
├── armeabi-v7a
│   ├── gdb.setup
│   ├── gdbserver
│   └── libnative.so
├── mips
│   ├── gdb.setup
│   ├── gdbserver
│   └── libnative.so
├── mips64
│   ├── gdb.setup
│   ├── gdbserver
│   └── libnative.so
├── x86
│   ├── gdb.setup
│   ├── gdbserver
│   └── libnative.so
└── x86_64
    ├── gdb.setup
    ├── gdbserver
    └── libnative.so


其中 libnative.so 則是跟 android.ndk 裡定義的名字。

2015年4月2日 星期四

Android 開發筆記 - 嘗試取出 WebView Javascript console.log @ 小米2S Android 4.1.1

原先 Android 4.4 已經有漂亮的解法:Debugging Web Apps,但是,對於其他版本的就 糟糕了。

所幸小米2S的系統還算 ok,只需啟動開發者模式,並播打電話 *#*#717717#*#* 後,即可開啟完整的開發者模式,透過 adb 連線。

$ adb devices
...至少要看到小米2S機器

$ adb logcat com.android.chromium:* | grep 'WebViewerFragment'
...
D/WebViewerFragment(25584): onScroll:192.34418
D/WebViewerFragment(25584): onScroll:344.65582
D/WebViewerFragment(25584): onFling:-4593.181
D/WebViewerFragment(25584): onScroll:-71.85919
D/WebViewerFragment(25584): onScroll:-183.00793
D/WebViewerFragment(25584): onScroll:-61.014893
D/WebViewerFragment(25584): onScroll:-77.11798
D/WebViewerFragment(25584): onFling:5177.8477
D/WebViewerFragment(25584): onScroll:-42.316345
D/WebViewerFragment(25584): onScroll:-70.78821
D/WebViewerFragment(25584): onScroll:-89.440186
D/WebViewerFragment(25584): onScroll:-105.551575
D/WebViewerFragment(25584): onScroll:-117.46338
D/WebViewerFragment(25584): onFling:8612.66
...


如此,簡易版搞定 :P 上述訊息是 javascript console.log。另一外提的是,我把小米2s系統預設瀏覽器改成 chrom browser ,不知使用系統預設 browser 是否一樣,以及我安裝的 app 可能是 debug 版本,有一說 release 版本可能看不到。