MeeGo HarmattanでNFCタグを読み書きする

この投稿は、コンテスト「 Smart Posts for Smart Posts 」に参加しています



はじめに



NFCとは何ですか?


ウィキペディアによると、 NFC(近距離無線通信)は、約10センチの距離にあるデバイス間でデータを交換できる短距離無線高周波通信技術です。



携帯電話でのNFCテクノロジーの最も一般的な用途は3つあります。

カードエミュレーション-電話は、パスまたは支払いカードなどのカードのふりをします。

読み取りモード-電話は、たとえば、インタラクティブ広告のためにパッシブタグ(タグ)を読み取ります。

P2Pモード-2台の電話が通信して情報を交換します。



2番目の使用方法、つまりパッシブタグの読み取りを検討します。さらに、電話を使用してそのようなタグの情報を記録する方法も学習します。



ストーリーは何ですか?


NFCの操作方法だけでなく、この記事のために私が開発したユーザーインターフェイスについても説明します。 つまり、読み取りプロセス中に、MeeGo HarmattanのNFCタグを操作するためのアプリケーションを作成するための完全な方法を使用します。



目次







Qtアンバサダー

更新:本日、 201112月20日、 Qt Ambassadorで申請が受理されたという手紙が届きました

更新:昨夜、プロジェクトはプログラムで公開されました: Qt Ambassador Showcase







NFCタグとは何ですか?


NFCタグ -これはパッシブタグです。 写真はどのように見えるかの外観を示しています。つまり、原則として、マイクロチップとフォイルアンテナが組み込まれた厚い紙でできたステッカーです。 NFCタグにはいくつかのタイプがあり、最大許容データサイズもタイプによって異なります。 私は、 Qt Developer Days 2011から持ってきた192バイトのいくつかのType 2タグの幸運な所有者です。 192バイトは太くありませんが、実験には十分です。



プログラムロジック



アプリケーションがNFCタグの処理をインターセプトする


したがって、タグの処理を開始するには、 QNdefManagerクラスのオブジェクトが必要です

NfcManager::NfcManager(QObject *parent) : QObject(parent), m_manager(new QNearFieldManager(this)), m_cachedTarget(0), m_mode(NfcManager::Read) { connect(m_manager, SIGNAL(targetDetected(QNearFieldTarget*)), this, SLOT(targetDetected(QNearFieldTarget*))); connect(m_manager, SIGNAL(targetLost(QNearFieldTarget*)), this, SLOT(targetLost(QNearFieldTarget*))); m_manager->setTargetAccessModes(QNearFieldManager::NdefReadTargetAccess | QNearFieldManager::NdefWriteTargetAccess); }
      
      





NfcManagerクラスのコンストラクターで作成してみましょう。これは、NFCを操作するために使用します。 このオブジェクトのtargetDetected信号targetLost信号をスロットに接続する必要があります。実際には、タグは電話の視野に表示されるイベントのメインハンドラーになります。 コンストラクターの3行目では、読み取りモードと書き込みモードを設定して、タグの読み取りだけでなく書き込みもできるようにします。



インターセプター


次に、説明されているスロットを検討します。

 void NfcManager::targetDetected(QNearFieldTarget *target) { if (m_cachedTarget) delete m_cachedTarget; m_cachedTarget = target; if (m_mode == Read) readTarget(m_cachedTarget); if (m_mode == Write) writeTarget(m_cachedTarget); }
      
      





タグが見つかった場合、 念のため、タグ自体へのプログラムインターフェイスであるQNearFieldTarget型のオブジェクトへのポインターを保存します。

2つの条件が続き、モード(読み取りまたは書き込み)に応じて、対応する処理メソッドを呼び出します。 美しいアーキテクチャの観点から見ると、これは最良の解決策ではありませんが、コードを複雑にしないように意図的に行いました。



 void NfcManager::targetLost(QNearFieldTarget *target) { m_cachedTarget = 0; target->deleteLater(); }
      
      





タグが失われると、使い果たされたリソースが解放されます。



読書


次に、タグの読み取り方法を見てみましょう。

 void NfcManager::readTarget(QNearFieldTarget *target) { connect(target, SIGNAL(error(QNearFieldTarget::Error,QNearFieldTarget::RequestId)), this, SLOT(errorHandler(QNearFieldTarget::Error,QNearFieldTarget::RequestId))); connect(target, SIGNAL(ndefMessageRead(QNdefMessage)), this, SLOT(readRecords(QNdefMessage))); target->readNdefMessages(); }
      
      





読み取りは非同期モードで行われるため、この方法では、エラー処理信号と読み取り完了信号を接続します。読み取り完了信号は、読み取りがエラーなしで発生した場合にのみ呼び出されます。

その後、メソッドを呼び出して読み取ります。

 void NfcManager::readRecords(const QNdefMessage &message) { if (message.isEmpty()) return; QNdefRecord record = message.at(0); // Read only first readRecord(record); }
      
      





読み取りが成功した場合、このスロットに移動し、タグに存在するエントリのリストから最初のレコードを取得します。

はい、仕様によると、タグには複数のレコードを含めることができますが、ドキュメントに記載されているように、 SymbianHarmattanでは 1つのレコードのみを読み書きできます。



 void NfcManager::readRecord(const QtMobility::QNdefRecord &record) { DataContainer *result = 0; if (record.isRecordType<QNdefNfcUriRecord>()) { QNdefNfcUriRecord uriRecord(record); result = new UriDataContainer(uriRecord.payload(), uriRecord.uri().toString()); } else if (record.isRecordType<QNdefNfcTextRecord>()) { QNdefNfcTextRecord textRecord(record); result = new TextDataContainer(textRecord.payload(), textRecord.text()); } else if (record.isRecordType<NdefNfcSmartPosterRecord>()) { NdefNfcSmartPosterRecord smartPosterRecord(record); result = new SmartPosterDataContainer(smartPosterRecord.payload(), smartPosterRecord.uri().toString(), smartPosterRecord.title()); } else { result = new DataContainer(record.payload()); } emit tagReadFinished(result); }
      
      





そして、補助メソッドのいくつかの移行の後、最も重要なメソッドに到達しました。このメソッドは、タグにエンコードされた情報を使い慣れた文字に変換します。

現時点では、 Qt Mobilityはすぐに使用できるので、リンク( Uri )とテキスト( Text )の2種類のレコードのみをサポートしています。3番目の種類であるスマートポスターに戻ります。

ご覧のとおり、レコードのデータはすぐに新しいオブジェクトに配置されます。これらは、 QMLでのデータ転送を容易にするために特別に作成した単純なオブジェクトです



最後に、データを持つオブジェクトを含む信号が呼び出されます。 将来、この信号をQMLでキャッチします。



記録


 void NfcManager::setDataForWrite(const QString &text, const QString &uri) { m_textForWrite = text; m_uriForWrite = uri; }
      
      





このメソッドは、新しいUriやTextの値を設定するために、書き込みを試みる前に呼び出す必要があります。 タグで呼び出さない場合、以前のデータが書き込まれます(このアプローチは、同じタイプのタグをたくさん書く必要がある場合に便利です)



 void NfcManager::writeTarget(QNearFieldTarget *target) { if (m_textForWrite.isEmpty() && m_uriForWrite.isEmpty()) return; m_cachedTarget = target; QNdefMessage message; if (!m_textForWrite.isEmpty() && !m_uriForWrite.isEmpty()) { NdefNfcSmartPosterRecord smartPosterRecord; smartPosterRecord.setTitle(m_textForWrite); smartPosterRecord.setUri(QUrl(m_uriForWrite)); message.append(smartPosterRecord); } else if (!m_textForWrite.isEmpty()) { QNdefNfcTextRecord textRecord; textRecord.setText(m_textForWrite); message.append(textRecord); } else { QNdefNfcUriRecord uriRecord; uriRecord.setUri(QUrl(m_uriForWrite)); message.append(uriRecord); } connect(target, SIGNAL(error(QNearFieldTarget::Error,QNearFieldTarget::RequestId)), this, SLOT(errorHandler(QNearFieldTarget::Error,QNearFieldTarget::RequestId))); connect(target, SIGNAL(ndefMessagesWritten()), this, SIGNAL(tagWriteFinished())); target->writeNdefMessages(QList<QNdefMessage>() << message); }
      
      





メインの書き込み方法は、読み取り方法ほど複雑ではありません。 条件ブロックでは、単にレコードのタイプを選択します。 UriまたはTextのみが存在する場合、対応するタイプが作成され、両方のフィールドが入力されると、スマートポスタータイプのレコードが作成されます。

その後、エラーハンドラを再接続します。 ただし、バックエンドでは読み取りの正常な完了を処理するためのロジックは必要ないため、シグナルをシグナルに転送します。これは後でQMLでキャッチします。



スマートポスター、それは何ですか?


そのため、スマートポスターは特殊なタイプのNFC記録であり、リンク、テキスト見出し(複数の言語)、 jpegまたはpng形式のグラフィックアイコン、さらにはmpeg形式のアニメーションアイコンの両方を含めることができます。

さらに、さらに2つのフィールドが存在する場合があります。

アクション -uriRecordを処理するためにどのアプリケーションとどのように開くかを電話に伝えます

サイズ -ダウンロードしたコンテンツのサイズを参照によって表示する単純な整数。



スマートポスターのクラスを書く


以下に、スマートポスターレコードのタイプを作成する例を使用して 、独自のNDEFレコードタイプ作成する方法を説明します。

すぐに私のタイプが簡略化されるように予約してください。 アクション、サイズ、アイコンのいずれもサポートしていませんが、テキストとリンクを同時に保存できます。



スマートポスタークラスの発表は次のようになります。

 class NdefNfcSmartPosterRecord : public QNdefRecord { public: Q_DECLARE_NDEF_RECORD(NdefNfcSmartPosterRecord, QNdefRecord::NfcRtd, "Sp", QByteArray()) void setTitle(const QString &title, const QString &locale = "en"); void setUri(const QUrl &uri); QString title(const QString &locale = "en") const; QUrl uri() const; //TODO: Add icon, action and size fields support private: RecordPart readPart(int &offset) const; }; Q_DECLARE_ISRECORDTYPE_FOR_NDEF_RECORD(NdefNfcSmartPosterRecord, QNdefRecord::NfcRtd, "Sp")
      
      





そのため、Qt Mobilityの開発者は、私たちが住みやすいようにすでに世話をしており、すべての大まかな作業を行う2つの特別なマクロを作成しています。



マクロのパラメーターは次のとおりです。クラス名、レコードタイプ(スマートポスターの場合はQNdefRecord :: NfcRtd )および「タイプ名」-タグでの認識の省略形。 また、 Q_DECLARE_NDEF_RECORDの最後のパラメーターは、初期データ初期化のデータです。この場合、空のバイト配列です。



次に、読み取りおよび書き込みメソッドの実装を見てみましょう。



レコードの解析された部分を保存するための単純な構造:

 struct RecordPart { enum Type { Uri, Text, Action, Icon, Size, Unknown }; Type type; QString text; QString locale; // For text type quint8 prefix; // For Uri type RecordPart() : type(Unknown), text(QString()), locale(QString()), prefix(0) { } };
      
      







まず、読み取り方法を検討します。

 static const char * const abbreviations[] = { 0, "http://www.", "https://www.", "http://", //    "urn:epc:", "urn:nfc:", };
      
      





仕様でサポートされているさまざまなURIプレフィックスの配列。



 QUrl NdefNfcSmartPosterRecord::uri() const { const QByteArray p = payload(); if (p.isEmpty()) return QUrl(); if (p.isEmpty()) return QString(); int offset = 0; QString uri; while (offset < p.size()) { RecordPart part = readPart(offset); if (part.type == RecordPart::Uri) { if (part.prefix > 0 && part.prefix < (sizeof(abbreviations) / sizeof(*abbreviations))) uri = QString(abbreviations[part.prefix]) + part.text; } } if (uri.isEmpty()) return QUrl(); return QUrl(uri); }
      
      





uriの読み取り方法は一見非常に単純です。レコードから読み取ったすべてのバイトをpにロードし、Uri型の部分が見つかるまで配列内の部分を読み取ります(仕様では、1つのみ存在できます)

「魔法の」 readPartメソッドについては、以下で説明します。



 QString NdefNfcSmartPosterRecord::title(const QString &locale) const { const QByteArray p = payload(); if (p.isEmpty()) return QString(); int offset = 0; QMap<QString, QString> title; while (offset < p.size()) { RecordPart part = readPart(offset); if (part.type == RecordPart::Text) { title.insert(part.locale, part.text); } } if (title.isEmpty()) return QString(); if (title.contains(locale)) return title.value(locale); if (title.contains("en")) return title.value("en"); return title.constBegin().value(); }
      
      





タイトルの方法は、異なる言語で多くのタイトルが存在できるという点でのみ異なります。 したがって、最初にすべてを選択してから、正しいものを見つけようとします。



すべての魔法はreadPartメソッドで発生します。このメソッドは、内部記録形式をシンプルでわかりやすいRecordPart構造に変換します

 RecordPart NdefNfcSmartPosterRecord::readPart(int &offset) const { RecordPart result; const QByteArray p = payload(); ..... //This block has pointer arithmetic, don't edit quint8 typeLength = p[++offset]; quint8 payloadLength = p[++offset]; QString type = QString(p.mid(++offset, typeLength)); offset += typeLength - 1; if (type == "U") { result.type = RecordPart::Uri; result.prefix = p[++offset]; result.text = QString(p.mid(++offset, payloadLength - 1)); offset += payloadLength - 1; } if (type == "T") { result.type = RecordPart::Text; quint8 localeLength = p[++offset]; result.locale = QString(p.mid(++offset, localeLength)); // 5 bytes of locale string offset += localeLength - 1; result.text = QString(p.mid(++offset, payloadLength - 1 - localeLength)); offset += payloadLength - 1 - localeLength; } ..... //TODO: Add handler for icon return result; }
      
      





各ブロックのタイトルは以下で構成されています:

1バイト、テクニカルフラグ、簡略化されたクラスでは、整合性などをチェックしないため、このバイトをスキップします。

2バイト、 「タイプ名」を含むストリング長。

3バイト、情報を含むメインフィールドの長さ。

4-nバイト、 「タイプ名」の文字列-文字列タイプ識別子、テキストの場合は「T」、ウリの場合は「U」

次はメインデータブロックです。



ウリの場合、これらは2つのフィールドにすぎません

上記の配列のほか、リンク本体へのすべてのプレフィックス番号ごとに1バイト。



テキストの場合、これらは3つのフィールドです。

追加のフラグとロケールの長さを含む1バイトのステータスフィールド。

en 」や「 ru-RU 」などのロケール文字列

そして、実際には、テキスト自体。



このメソッドは、非定数の参照によってオフセットを取得し、変更するため、ループ内であるレコードから別のレコードに移動できることに注意してください。



次に、録音方法について説明します。 簡単にするために、 setUriのみを考慮します。 タイトルのメソッドは比較的同じです。

 void NdefNfcSmartPosterRecord::setUri(const QUrl &uri) { //Don't edit - pointer arithmetic QByteArray p; int abbrevs = sizeof(abbreviations) / sizeof(*abbreviations); for (int i = 1; i < abbrevs; ++i) { if (uri.toString().startsWith(QLatin1String(abbreviations[i]))) { p[0] = i; p += uri.toString().mid(qstrlen(abbreviations[i])).toUtf8(); } } QByteArray oldPayload = payload(); QByteArray uHeader(4, 0); uHeader[0] = 0b01 + 0b00010000; uHeader[1] = 1; uHeader[2] = p.size(); uHeader[3] = 'U'; if (!oldPayload.isEmpty()) { uHeader[0] = uHeader[0] + 0b10000000; // change MB flag here oldPayload[0] = oldPayload[0] & 0b01111111; } if (oldPayload.isEmpty()) { uHeader[0] = uHeader[0] + 0b10000000 + 0b01000000; } p.prepend(uHeader); p.append(oldPayload); setPayload(p); }
      
      





インストール方法の複雑さは、Smart Posterの一部(テキストなど)が既にインストールされている場合を考慮する必要があることです。また、今度はUriもインストールする必要があります。 これは、古いペイロードを保存して新しいペイロードを追加する必要があることを意味します。 2つのQByteArrayを連結しても問題はないように見えますが、ここではフラグ付きの最初のバイトが作用します。事実、新しいものを追加するときに最初の部分( MB )のフラグを変更する必要があります。

このコード行はこれを行います:

 // change MB flag here oldPayload[0] = oldPayload[0] & 0b01111111;
      
      





ご覧のとおり、新しいパーツを古いパーツの前に追加し、後には追加しません。 これは、最後に追加した場合、最後の部分のフラグとその変更( ME )を検索する必要があるためです。

古いペイロードを回避する必要があります。



それはすべて、スマートポスターと一般的なNFCについてです。



プログラムインターフェース



PageおよびPageStack


QMLのモバイルアプリケーションの主なアイデアは、キュー内の画面を切り替えることです。 Qtコンポーネントに関しては、画面はページと呼ばれ、メインコンテナはウィンドウです。

 import QtQuick 1.1 import com.nokia.meego 1.0 PageStackWindow { id: appWindow initialPage: mainPage MainPage { id: mainPage } }
      
      





main.qmlはここにコンテナウィンドウを作成し、メインページが初期化ページとして指定されます。



 Page { id: mainPage ..... Header { id: header anchors { top: parent.top right: parent.right left: parent.left } } .....
      
      





これはページの説明です。 ところで、気がついたなら、ノキアのすべての標準アプリケーションにはきれいな色のヘッダーがあります。 UIガイドラインのどこでも使用することを推奨しているにもかかわらず、このヘッダーには標準コンポーネントはありません。



ページ間を移動するには、タイプPageStackのオブジェクトが使用され 、すべてのページにはpageStackという名前のこのクラスのインスタンスへのポインターがあります。 したがって、新しいページに移動するには、次の構造を使用する必要があります

 pageStack.push(Qt.resolvedUrl("NewPage.qml"))
      
      







前のものに戻るには:

 pageStack.pop()
      
      





ところで、popメソッドに特定のページの識別子を設定すると、ページに戻るだけでなく、スタック上の任意のページに戻ることができます。



リストビュー


メイン画面で、実行可能なアクションのリストを確認できます。同様のリストが次のように実行されます。

 ListView { id: actionList .... delegate: ListDelegate { anchors { left : parent.left leftMargin: 20 } onClicked: { pageStack.push(Qt.resolvedUrl(model.source)) } MoreIndicator { anchors { verticalCenter: parent.verticalCenter right: parent.right rightMargin: 30 } } } model: ListModel { ListElement { title: "Read Tag" subtitle: "" source: "ReadPage.qml" } ...... } }
      
      





ListView要素は実際のリスト自体であり、2つの重要なプロパティがあります。

デリゲートは1つのリストアイテムをレンダリングするためのデリゲートであり、 モデルはリストのデータモデルです。

com.nokia.extrasパッケージには、単純なデリゲートを作成するための既製のListDelegateコンポーネントが含まれています。 ListModel要素を使用すると、単純なデータモデルを指定できます。 また、 ListElementはこのモデルの1つのレコードにすぎません。



ツールバー


さまざまなアクションのために、モバイルアプリケーションにはアイコン付きのツールバーもあります。私のアプリケーションはシンプルで、内部ページのツールバーには戻るボタンしかありません

 Page { id: readPage ..... tools: ToolBarLayout { ToolIcon { iconId: "toolbar-back" onClicked: { pageStack.pop() } } } .....
      
      





ツールバーをページに接続するには、デフォルトでnullであるtoolsプロパティにツールバーを割り当てる必要があります



ラベルとテキストフィールド


テキストを表示するには、 Labelコンポーネントを使用できます。これは、標準のText要素の定型化されたラッパーにすぎません。

 Label { id: touchLabel ..... font.pixelSize: 60 text: qsTr("Touch a tag") }
      
      







また、入力フィールドにはTextFieldを使用する必要があります -これは標準のTextInputの高度なラッパーです

 TextField { id: textEdit ..... placeholderText: qsTr("Text") text: "yandex" }
      
      







インフォバナー


タグの読み取り/書き込み中にエラーが発生した場合、何らかの方法でユーザーに通知し、再度タグを電話に持ってくるように要求する必要があります。InfoBanner要素を使用できます

 InfoBanner{ id: errorBanner timerEnabled: true timerShowTime: 3 * 1000 topMargin: header.height + 20 leftMargin: 20 }
      
      







すべてをまとめる



アプリケーションに必要なすべてのメインQMLコンポーネントと、必要なすべてのプログラムロジックを個別に調べました。 両方の部分をまとめます。



setContextProperty


QMLコードが読み取りおよび書き込みコントロールのクラスを確認するには、このクラスのオブジェクトの存在について宣言エンジンに通知する必要があるため、main.cppに次のように記述します。

 NfcManager *nfcManager = new NfcManager(); viewer->rootContext()->setContextProperty("NfcManager", nfcManager);
      
      





つまり、NfcManagerオブジェクトを作成し、QMLからアクセスする必要があることをエンジンに伝えます。



ところで、最新のQtSDKアップデートで何かが壊れました。このコードが正しく機能するためには、バグトラッカーで説明されている回避策を使用する必要があります。



qmlRegisterType


もちろん、ラベルを読み取った後、受信データを含むオブジェクトを含む信号を送信することを忘れないでください。 このオブジェクトをQMLで使用可能にするには、このオブジェクトのクラスをQMLに登録する必要があります

 qmlRegisterType<DataContainer>(); qmlRegisterType<UriDataContainer>(); qmlRegisterType<TextDataContainer>(); qmlRegisterType<SmartPosterDataContainer>();
      
      





このコードをmain.cppに挿入したら 、所有するすべてのタイプのデータのデータクラスを登録します。

ただし、このようなオブジェクトをQMLから直接作成することは禁止されています。



相互作用


ユーザーがページにアクセスしてタグの書き込みまたは読み取りを行う場合、次のコードを実行する必要があります。



読むために

 function tagWasRead(container) { NfcManager.stopDetection() readPage.dataContainer = container pageStack.push(Qt.resolvedUrl("ReadResultPage.qml"), {dataContainer: readPage.dataContainer}) } function readError(string) { errorBanner.text = string errorBanner.show() } Component.onCompleted: { NfcManager.tagReadFinished.connect(readPage.tagWasRead) NfcManager.accessError.connect(readPage.readError) NfcManager.setReadMode() NfcManager.startDetection() }
      
      





Component.onCompletedメソッドは、ページが完全に作成されると実行されます。 このメソッドでは、エラーおよび成功した結果のハンドラーをNfcManagerからのシグナルにフックします(C ++シグナルをQMLスロットに接続するための構文に注意してください)

その後、読み取りモードを設定し、タグを添付することをマネージャーに通知します。



プッシュコールにも注意してください

 pageStack.push(Qt.resolvedUrl("ReadResultPage.qml"), {dataContainer: readPage.dataContainer})
      
      





2番目のパラメーターにより、データコンテナーを次のページに渡すことができます。



例:

 ..... Label { id: rawDataLabel width: parent.width font.pixelSize: 30 font.family: "Courier New" text: readPage.dataContainer.rawHexData() wrapMode: Text.WrapAnywhere } .....
      
      







記録する

 function tagWasWritten() { ..... } function writeError(string) { ..... } Component.onCompleted: { NfcManager.tagWriteFinished.connect(writePage.tagWasWritten) NfcManager.accessError.connect(writePage.writeError) NfcManager.setWriteMode() NfcManager.setDataForWrite(writePage.text, writePage.uri) NfcManager.startDetection() }
      
      





よく似ていますよね? 唯一の違いは、書き込み用のデータを渡すsetDataForWriteメソッドの呼び出しです。



おわりに



したがって、MeeGo Harmattanプラットフォーム用のシンプルで機能的なアプリケーションができました。 ただし、最小限の労力でSymbianのアプリケーションに変えることができます。 私の知る限り、一部のSymbian携帯電話( C7など)にはNFCチップが組み込まれています。

また、NFCタグに正式に追加したいので、アプリケーションでしか理解できないような形式で情報を書き込むことができます。 したがって、この技術を使用するより多くの方法を思い付くことができます。



何を読む


このトピックに興味がある場合は、NFCおよびNDEFの公式仕様に精通することをお勧めします。 これらは、このページから完全に無料でオンデマンドでダウンロードできます

Qt ConnectivityのドキュメントはQtSDKに含まれていますが、たとえば、独自のQNdefRecord形式を開発する場合、それだけでは不十分な場合があります。QtMobilityソースを歓迎します-多くの興味深いことが見つかります。

MeeGo Qt Componentsによると、QtSDK 公式ドキュメントも共存しますが、時には多くのことが望まれる場合があります



オプショナル


私はアプリケーションの開発を続けますが、これはおそらくNFCタグの最後の投稿ではありません。

最新情報を入手するには、 ギトリウスのプロジェクトをご覧ください

または、私のブログを購読してください。このリンクはプロファイルにあります。

近い将来、 Nokia Storeにアプリケーションを配置する予定なので、そこを探してください。

debパッケージはここからダウンロードできます



謝辞


ユーザーのエラーや誤植について記事のテキストを校正してくれたことに感謝します。

dreary_eyestass



All Articles