- 投稿日:2020-02-08T20:23:32+09:00
android.os.HandlerのpostとpostDelayedを使った際のQueueに積まれる順番について
AndroidでHandlerの
post(Runnable r)
を複数回コールした場合は、postした順にQueueに積まれて、実行されます。post(Runnable r)
とpostDelayed(Runnable r, long delayMillis)
を使った場合にどうQueueに積まれるか調べてみました。ソースコードを見ると、postとpostDelayedの内部では共に
sendMessageDelayed(Message msg, long delayMillis)
が呼ばれていました。Handler.javapublic final boolean post(@NonNull Runnable r) { return sendMessageDelayed(getPostMessage(r), 0); } public final boolean postDelayed(@NonNull Runnable r, long delayMillis) { return sendMessageDelayed(getPostMessage(r), delayMillis); }sendMessageDelayedの中では
sendMessageAtTime(Message msg, long uptimeMillis)
が呼ばれており、引数のuptimeMillisにはSystemClock.uptimeMillis() + delayMillis
が設定されています。Handler.javapublic final boolean sendMessageDelayed(Message msg, long delayMillis) { if (delayMillis < 0) { delayMillis = 0; } return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis); }つまり、
postDelayed(Runnable r, long delayMillis)
はdelayMillis ミリ秒後にpost(Runnable r)
をするのと同じことになります。
例えば、下記のコードを実行すると、Sample.ktval handler = Handler() handler.postDelayed( Runnable { Log.d(TAG, "Runnable 1") }, 100) Log.d(TAG, "1 postDelayed") handler.post( Runnable { Log.d(TAG, "Runnable 2-1") Thread.sleep(100) Log.d(TAG, "Runnable 2-2") }) Log.d(TAG, "2 post") handler.post( Runnable { Log.d(TAG, "Runnable 3-1") Thread.sleep(200) Log.d(TAG, "Runnable 3-2") }) Log.d(TAG, "3 post")ログは下記のようになります。
2020-02-08 20:08:07.385 22994-22994/Sample: 1 postDelayed 2020-02-08 20:08:07.385 22994-22994/Sample: 2 post 2020-02-08 20:08:07.385 22994-22994/Sample: 3 post 2020-02-08 20:08:07.490 22994-22994/Sample: Runnable 2-1 2020-02-08 20:08:07.591 22994-22994/Sample: Runnable 2-2 2020-02-08 20:08:07.592 22994-22994/Sample: Runnable 3-1 2020-02-08 20:08:07.793 22994-22994/Sample: Runnable 3-2 2020-02-08 20:08:07.950 22994-22994/Sample: Runnable 1
Runnable 1
はRunnable 3
の後に実行されます。
また、postDelayed
はdelayMillisを100msにしていますが、実際に実行されるのは565ms後です。
Runnable 1
が、Runnable 3
の前または、100ms後に実行されることを想定していると、思わぬ不具合を生むことになるので注意が必要です。
- 投稿日:2020-02-08T17:59:57+09:00
React Nativeでスマートフォンのカレンダーのデータを取得
調査内容
Android/iOS共にNativeコードではスマートフォンのカレンダーデータにアクセスする事ができますが、React Nativeでアクセスする方法について調査しました。今回はAndroidでのみ動作確認を行いましたが、iOSでも動作可能と思われます。
設定手順
以下のモジュールを利用します。
https://github.com/wmcmahan/react-native-calendar-events$ npm install --save react-native-calendar-events $ react-native link今回はAndroidで動作確認をしますので、AndroidのNative部分をいくつか修正します。
android/settings.gradleに以下を追加
include ':react-native-calendar-events' project(':react-native-calendar-events').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-calendar-events/android')AndroidManifest.xmlに以下を追加
<uses-permission android:name="android.permission.READ_CALENDAR" /> <uses-permission android:name="android.permission.WRITE_CALENDAR" />MainActivity.javaに以下を追加
@Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { CalendarEventsPackage.onRequestPermissionsResult(requestCode, permissions, grantResults); super.onRequestPermissionsResult(requestCode, permissions, grantResults); }React Nativeのアプリケーションを記述
App.jsからMainScreen.jsを呼び出す形に変更して、MainScreen.jsに3箇所押せるようにTouchableViewを利用します。
/* App.js */ import React from 'react'; import { StyleSheet, Button, View, } from 'react-native'; import MainScreen from './src/screens/MainScreen' const App: () => React$Node = () => { return ( <View> <MainScreen /> </View> ); }; const styles = StyleSheet.create({ }); export default App;/* MainScreen.js */ class MainScreen extends React.Component { render() { return ( <View style={styles.container}> <TouchableHighlight style={styles.listItem} onPress={this.authorizeEventStore}> <Text style={styles.title}>パーミッションリクエスト</Text> </TouchableHighlight> <TouchableHighlight style={styles.listItem} onPress={this.findCalendars}> <Text style={styles.title}>カレンダー取得</Text> </TouchableHighlight> <TouchableHighlight style={styles.listItem} onPress={this.fetchEvents}> <Text style={styles.title}>イベント取得</Text> </TouchableHighlight> </View> ); } } const styles = StyleSheet.create({ container: { width: '100%', flex: 1, }, listItem: { padding: 32, borderBottomWidth: 1, borderBottomColor: '#ddd', backgroundColor: '#fff', justifyContent: 'center', }, title: { fontSize: 20, }, });コードの記述。
それぞれに応じた関数を以下のように記述します。Androidではtarget SDK 23以降はパーミッションの確認が必要なので、RNCalendarEvents.authorizeEventStore()でパーミッションを取得します。
authorizeEventStore() { console.log("confirmAuthorizationStatus"); RNCalendarEvents.authorizeEventStore() .then((status) => { console.log(status); }) .catch((error) => { console.log(error); }); }パーミッションリクエストを押すと以下のようなパーミッションの確認がOSから表示されます。
後はCalendarの取得とイベントの取得を以下のように記述します。RNCalendarEvents.fetchAllEvents(startDate, endDate, calendars)ではcalendarsでカレンダーIDを指定すれば良いですが、今回は簡略化のためにハードコーディングしています。ボタンを押すと取得された結果がConsole上で出力されます。
findCalendars() { RNCalendarEvents.findCalendars() .then((list) => { console.log(list); }) .catch((error) => { console.log(error); }); } fetchEvents() { RNCalendarEvents.fetchAllEvents('2019-01-01T17:24:00.000Z', '2019-12-31T17:24:00.000Z,'[2]) .then((list) => { console.log(list); }) .catch((error) => { console.log(error); }) }以上、react-native-calendar-eventsを使う事で簡単にスマートフォンのカレンダーにアクセスする事が可能となりました。
- 投稿日:2020-02-08T17:32:47+09:00
AndroidでBluetooth関係のAPIを使ってPAPERANGに接続してみる
はじめに
正月に秋葉原行ってじゃんぱらでPAPERANGを購入。
で、PAPERANGのAPIないかなと探したけど、Cordovaのプラグインしか見つからなかったので、プラグインのソースをAndroidStudioで普通にアプリに使えないか試してみる。
もし、もっと良い方法を知ってる方がいたらコメントください。うまく使えたら、Androidの会で何か作るときや、ハッカソンなんかで使ってみたい。
CordovaのPAPERANG pluginを見つけて、その中のjarを使ってできないか試したが、なんか、メーカーからIDとかもらわないとできそうもないのであきらめ、別の方法を模索。
Androidの会 浜松支部の定例会で、話をしていたところ、よさげなページを見つたので、そこを参考に紙送りができるところまで確認できた。良さげなページは以下のサイト
M5StickC(ESP32)からBluetooth小型ポータブルレシートプリンタ「PAPERANG」を制御する
Bluetooth小型ポータブルレシートプリンタ「PAPERANG」
miaomiaoji-tool
ちゃんと使っていくためには、miaomiaoji-toolのコードを確認していかないといけない。AndroidManifest.xml
こんな感じ
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="krohigewagma.jp.paperangsample"> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <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> </manifest>コード
とりあえずのお試し適当コード。
接続履歴を使って、PAPERANGを探して接続するダサいコード。
本当は、Bluetoothで接続されているデバイスの中から選ばないといけない
ボタンを押すと、紙送りされます。public class MainActivity extends AppCompatActivity { // CRC private static byte[] crc = new byte[]{ (byte)0x02, (byte)0x18, (byte)0x00, (byte)0x04, (byte)0x00, (byte)0x78, (byte)0x7A, (byte)0xCE, (byte)0x33, (byte)0x2C, (byte)0x89, (byte)0x80, (byte)0xF0, (byte)0x03 }; // 紙送り byte[] data = new byte[]{ (byte)0x02, // (byte)0x1a, (byte)0x00, // 制御コード (byte)0x02, (byte)0x00, // データ長さ (byte)',' , (byte)0x01, // 紙送り量 (byte)0x8b, (byte)'V', (byte)'#', (byte)'T', // CRC (byte)0x03 // }; private static String SPP = "00001101-0000-1000-8000-00805F9B34FB"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button btn = findViewById(R.id.btnInit); btn.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent motionEvent) { Context appContext = getApplicationContext(); /* // Cordovaのプラグインのjarを使ってやったコード。 // ダメだったけど。とりあえず記念の残しておくけど後で消す String packageName = appContext.getPackageName(); PaperangApi.init(appContext, packageName, new OnInitStatusListener() { @Override public void initStatus(boolean b) { Log.e("PAPERANG", "b = " + b); } }); PaperangApi.registerBT(appContext); scan(); // PaperangApi.unregisterBT(appContext); // if(register()){ // Log.i("PAPERANG", "register success"); // scan(); // disconnect(); // }else{ // Log.i("PAPERANG", "register faild"); // } */ // お試しなので、イケてないコードになってしまった・・・ BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter(); if(bt.equals(null)){ Log.i("PAPERANG", "Bluetooth not support"); return false; } if(!bt.isEnabled()){ Log.i("PAPERANG", "disable bluetooth"); return false; } Set<BluetoothDevice> devices = bt.getBondedDevices(); List<String> macAddressList = new ArrayList<String>(); for(BluetoothDevice device : devices){ Log.i("PAPERANG", device.getAddress() + device.getName()); if(!"Paperang".equals(device.getName())){ continue; } macAddressList.add(device.getAddress()); BluetoothSocket socket = null; try{ socket = device.createRfcommSocketToServiceRecord(UUID.fromString(SPP)); socket.connect(); Log.i("PAPERANG", "connected."); OutputStream outst = socket.getOutputStream(); outst.write(crc); outst.write(data); }catch(IOException e){ Log.e("PAPERANG", e.getMessage()); }finally { try{ if(socket != null){ socket.close(); } }catch(IOException e2){ Log.e("PAPERANG", e2.getMessage()); } } return false; } return false; } }); } }おわり
次は印刷したい!
- 投稿日:2020-02-08T17:00:27+09:00
Flutter MethodChannel APIの使い方
はじめに
FlutterはクロスプラットフォームなUIフレームワークです。UI特化のため、UI作る以外のプラットフォーム固有の機能を利用する場合には、公開されているライブラリ (プラグイン) を利用する以外にはFlutterの専用APIを利用してプラットフォーム側の実装を行う必要があります。
プラットフォーム固有機能の例は以下です。
- Audio/Videoなどのメディアのデコード, エンコード, 再生
- ランタイムパーミッションの表示や設定確認
- Bluetoothや加速度センサーなどのハードウェア機能
- WebView
- ストレージやファイルアクセス
Dart⇆プラットフォーム双方向でまとまった情報が無かったため、ここにまとめることにしました。
今回はいくつかあるAPIのうち、最もよく使うMethodChannelについて使い方を解説します。関連記事
Flutter (Dart) とプラットフォーム (Android/iOSなど) 間の通信/呼び出しAPIについてまとめています。
- MethodChannel ← 今回
- EventChannel
- MessageChannel
1. Flutter MethodChannelとは
Dartからプラットフォーム (Android/iOS等) のメソッドを呼び出すもしくは、プラットフォームからDartのメソッドを呼び出すためのAPIです。イメージ的には、AndroidのJNI (Java/KotlinからC/C++を呼び出すI/F) に近いですが、違いはとても簡単に呼び出せるのと非同期APIだという点です。
2. MethodChannelの仕組み
MethodChannel APIの使い方は後述しますが、呼び出し先のメソッド名と引数のデータの2つを引数として渡します。Flutter Framework内ではMethodChannelをBinaryMessagesという形に変換し、Flutter Engineとメッセージパッシングのやり取りを行います。
この界面でAPIコールが非同期のメッセージパッシング (データ送受信) に変わります。
Flutter Engineはデータを受け取ると、そのデータをMethod ChannelのAPIの形に変更し、対象のプラットフォームのAPIをコールします。この仕組みを実現しているのがFlutter Engine内に存在するPlatform Channelsです。
3. 基本フロー
実際のコードの説明の前に、APIの基本的なフローについて解説します。ただし、プラットフォーム毎に若干API名が異なる場合があるため (iOS) 、注意してください。
3.1 Dart → プラットフォーム
- [プラットフォーム側] MethodChannel#setMethodCallHandlerでコールバックを登録
- [Dart側] MethodChannel#invokeMethodで呼び出したいメソッド名とデータをセットして非同期でコール
- [プラットフォーム側] 受け取ったデータ (メソッド名) を見て、対象の処理を実施し、Result#success (エラーの場合はerror) をコール
- [Dart側] メソッドコールの結果を確認
3.2 プラットフォーム → Dart
プラットフォーム側からDartを呼び出すことも出来ます。Dart → プラットフォームの場合と手順は同じです。
4. サンプルプロジェクト
実際のAPIの使い方は後述しますが、サンプルプロジェクトを以下に用意しています。そのまま動作確認可能ですので、参考にしてください。
ただし、正式に何かの機能を実装する場合は、プラグインの形で組み込んだ方が良いです。
https://github.com/Kurun-pan/flutter-methodchannel-example5. Flutter + Android (Kotlin) のコード解説
5.1 Dartからプラットフォーム (Kotlin) を呼び出すケース
5.1.1 Dart側の実装
通信チャンネル作成
MethodChannelで通信チャンネルを作成します。引数に指定する文字列は他のアプリケーションと被らず、一意に決まるようにするために慣例で"アプリパッケージ名/チャンネル名"とするのが一般的な様子です。
プラットフォーム側の呼び出し
チャンネル作成後はそのチャンネル#invokeMethodを非同期でコールすることで、Kotlin側を呼び出すことが出来ます。
APIの仕様は引数の仕様は以下のようになっています。
- 第一引数に呼び出したいメソッド名の文字列を指定
- 第二引数には呼び出しメソッドの引数に指定するデータを指定。
- 型はプリミティブ型で使える型に制約あり (StandardMessageCodec)
- 複数の引数を指定指定したい場合、JSON形式のようにMapで記述するのが便利かと
invokeMethod仕様Future<T> invokeMethod<T>(String method, [ dynamic arguments ]) asyncサンプルコード
実際のサンプルコードを以下に示します。
main.dartimport 'package:flutter/services.dart'; class _MyHomePageState extends State<MyHomePage> { static const MethodChannel _channel = const MethodChannel('com.example.methodchannel/interop'); static Future<dynamic> get _list async { final Map params = <String, dynamic> { 'name': 'my name is hoge', 'age': 25, }; final List<dynamic> list = await _channel.invokeMethod('getList', params); return list; } @override initState() { super.initState(); // Dart -> Platforms _list.then((value) => print(value)); }5.1.2 プラットフォーム側の実装
通信チャンネル作成
Dart側と同じように、MethodChannelで通信チャンネルを作成します。引数の文字列はDart側と同じにする必要があります。
メソッド名の取得
チャンネル作成後、MethodCallHanderのコールバックを設定します。コールバックメソッドの引数methodCall.metodに、Dart側のinvokeMethodの第1引数の文字列が格納されているため、それを見て適切な処理を行います。
引数の取得
Dart側のinvokeMethodの第2引数は、methodCall.argumentから取得出来ます。ただし、Dartとプラットフォーム間で受け渡し可能な型には限りがあるため、詳細はこちらを参照して下さい。もし非サポートの型を指定すると、実行時にエラーになります。
サンプルコード
Kotlinコードclass MainActivity: FlutterActivity() { companion object { private const val CHANNEL = "com.example.methodchannel/interop" private const val METHOD_GET_LIST = "getList" } private lateinit var channel: MethodChannel override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { GeneratedPluginRegistrant.registerWith(flutterEngine) channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) channel.setMethodCallHandler { methodCall: MethodCall, result: MethodChannel.Result -> if (methodCall.method == METHOD_GET_LIST) { val name = methodCall.argument<String>("name").toString() val age = methodCall.argument<Int>("age") Log.d("Android", "name = ${name}, age = $age") val list = listOf("data0", "data1", "data2") result.success(list) } else result.notImplemented() } }Dart側に結果を返す
プラットフォーム側からDart側に結果を返す場合は、MethodCallHanderのコールバックメソッドの第2引数のMethodChannel.Resultクラスインスタンスをコールすることで実現できます。サンプルコードのようにリターン値を渡すことも可能です。
受け取ったResultインスタンスをローカルに保存し、処理を行った後で結果を返すことも可能ですが、その場合には必ずUIスレッドで結果を返す必要があります。
参考のために以下にResultクラスの各メソッドのAPI仕様を掲載しておきます。
MethodChannel.Result.success仕様@UiThread void success(@Nullable Object result)MethodChannel.Result.error仕様@UiThread void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails)MethodChannel.Result.notImplemented仕様@UiThread void notImplemented()5.2 プラットフォーム (Kotlin) からDartを呼び出す
基本的にDartからプラットフォームを呼び出す方法と同じです。このメソッドの実行は必ずUIスレッドから行ってください。
プラットフォーム側の実装
作成したチャンネルに対して、invokeMethodをコールするだけです。
引数もプリミティブ型で渡すことが可能で、Dart側からの結果もMethodChannel.Resultのコールバックで受け取ることが可能です。不要なら指定なしでOKです。Kotlinコードchannel.invokeMethod("callMe", listOf("a", "b"), object : MethodChannel.Result { override fun success(result: Any?) { Log.d("Android", "result = $result") } override fun error(errorCode: String?, errorMessage: String?, errorDetails: Any?) { Log.d("Android", "$errorCode, $errorMessage, $errorDetails") } override fun notImplemented() { Log.d("Android", "notImplemented") } }) result.success(null)Dart側の実装
作成したチャンネル#setMethodCallHandlerでMethodCallを設定します。
メソッド名の取得
コールバック引数のcall.methodでプラットフォーム側から指定された第1引数の値を取得出来ます。
引数の取得
call.argumentsに格納されています。
結果のリターン
成功 (通常) の場合は、Future.valueで結果を返します。
Future.errorを利用すると、プラットフォーム側のMethodChannel.Result.errorがコールされます。
MethodChannel.Result.notImplementedの指定方法は、よく分かっていません。知っている方がいたら教えてください…。サンプルコード
DartコードFuture<dynamic> _platformCallHandler(MethodCall call) async { switch (call.method) { case 'callMe': print('call callMe : arguments = ${call.arguments}'); return Future.value('called from platform!'); //return Future.error('error message!!'); default: print('Unknowm method ${call.method}'); throw MissingPluginException(); break; } } @override initState() { super.initState(); // Platforms -> Dart _channel.setMethodCallHandler(_platformCallHandler); }6. 参考文献
- 投稿日:2020-02-08T00:26:38+09:00
Android でもとりあえず Ubuntu のデスクトップ環境を使いたい(UserLAnd 版)
はじめに
先に公開した Android Studio を使うための最低限のデスクトップ環境の構築方法を記載します。
日本語入力もありませんが、その代わり最短で構築できると思います。Android の上で最も手軽に Ubuntu の環境を構築するなら UserLAnd が一番楽だと思われます。
Termux でもできますが、UserLAnd の方が手間が少なくて済みます。
その代わり、Termux より動作速度が遅いです。
どちらが良いかはお手持ちのスマートフォンの速度と好みで選ぶと良いと思います。注意点
デスクトップ環境(LXDEやXfce)のインストールは数時間かかります。
時間を確保してから実施するほうが良いと思います。インストール
Play ストア で UserLAnd と XSDL をインストールしてください。
UserLAnd は Debian や Ubuntu といった Linux 環境を簡単に構築するためのアプリケーションになります。
XSDL は画面を担当するアプリケーションです。
UserLAnd はコマンドラインまでしかサポートしませんので、XSDL で画面をサポートしてもらいます。Ubuntu のインストール
UserLAnd を立ち上げて、Ubuntu と書かれた項目をタップします。
ユーザーIDとパスワードを入力して CONTINUE をタップします。
接続の種類に SSH, VNC, XSDLを選択するように言われますので、SSH を選択して CONTINUE をタップします。環境によりけりですが、1時間は待つことになると思います。
Ubuntu デスクトップ環境のインストール
ここからは Ubuntu での操作になります。
まずは apt でパッケージ(Linux アプリケーションの倉庫のようなもの)を更新します。
sudo apt update sudo apt upgrade -y次にデスクトップ環境をインストールします。
調べた限りでは、LXDE, Xfce の2種類が使えます。
LXDE の方が軽いそうですが、Xfce の方が見た目が良くLXDE程ではないですけれども十分な軽さを持っているそうです。
ここは好みで選択すれば良いと思います。なお、ここでネットワークやスマートフォンの速度にもよると思いますが数時間掛かります。
・ LXDE のインストール
sudo apt install -y lxde・ XFCE のインストール
sudo apt install -y xfce4環境の設定
XSDL を立ち上げてしばらく待って下さい。
最後なにやら文字列を表示している画面が出るので以下をメモしてください。上から2行目に以下の文字列が出るはずです。DISPLAY のポート番号をメモしてください。
export DISPLAY=(何かのIPアドレス):(ポート番号)
上から3行目に以下の文字列が出るはずです。同様に PULSE_SERVER のポート番号をメモしてください。
export PULSE_SERVER=(何かのIPアドレス):(ポート番号)
IPアドレスはメモする必要はないです。
再び UserLAnd の Ubuntu に戻って下さい。
XSDL に接続するために、設定ファイルに先程メモしたポート番号とローカルのIPアドレスを記載します。# テキストエディタ vim をインストールしていない人はインストール sudo apt install -y vim # startXDSL にXDSLの接続設定を記載する vim /support/startXSDLServerSteo2.sh4行目、8行目、16行目辺りを修正します。
# 4行目辺り。4721 の値をメモを行った DISPLAY のポート番号に書き換えます。 DISPLAY=:4721 ↓ 書き換え後 DISPLAY=:(さっきメモした DISPLAY のポート番号) # 8行目辺り。4721 の値をメモを行った PULSE_SERVER のポート番号に書き換えます。 PULSE_SERVER=localhost:4721 ↓ 書き換え後 PULSE_SERVER=localhost:(さっきメモした PULSE_SERVER のポート番号) # 16行目辺り。/usr/bin/twm の値を exec startlxde または、exec startyxfce4 に書き換えます。exec を付けないと動作しないので付けて下さい。 echo '/usr/bin/twm' > /home/$INITIAL_USERNAME/.xinitrc ↓ 書き換え後 # LXDE を使う場合 echo 'exec startlxde' > /home/$INITIAL_USERNAME/.xinitrc # Xfce を使う場合 echo 'exec startxfce4' > /home/$INITIAL_USERNAME/.xinitrcvim の操作方法を知らない人は以下だけ覚えればなんとかなります。
キーボードのキー 役割 i コマンドモードから入力モードに入る ESC 入力モードを抜けてコマンドモードに戻る :wq 保存して終了する。コマンドモードで操作すること。 デスクトップ環境の起動
UserLAnd に戻って Ubuntu を長押しし、Stop App を選んで終了して下さい。
(終了しない場合には、下の Sessionsにある Ubuntu を長押しして Stop Session を選択して下さい。)
再び Ubuntu を長押しし、App Info を選択し XSDL を選択してください。
下の Apps を押してから Ubuntu を押して起動してください。
しばらくすると Ubuntu のデスクトップ環境が立ち上がります。UserLAnd の場合には XSDL の立ち上げまで行ってくれるので、手動で XSDL を立ち上げる必要はありません。
参考サイト