しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

構成プロファイル(拡張子: mobileconfig)について調べてみる

格安SIMの初期設定時などによく使う構成プロファイル(拡張子: mobileconfig)について調べてみました。

構成プロファイルはMacApple Configuratorというアプリで作成します。

Apple Configuratorをダウンロードして開くと下のような表示になっています。

プロファイルはメニューの「ファイル」の「新規プロファイル」から作成します。

新規プロファイルを選ぶと下の画面に移動します。
この画面で色々な設定を行っていきます。

格安SIMで使われるモバイル通信以外にも、ドメインの監視・カレンダーの追加など様々な事を行う事が可能です。
一部設定は監視対象端末にしか適用できないので注意が必要です。

今回はWEBクリップを使ってホーム画面にアイコンを追加します。
設定は下の通りです。

アイコンは下URLのものを利用しました。

動物 | FLAT ICON DESIGN -フラットアイコンデザイン-

あとはメニューバーの保存を押せばmobileconfigファイルを保存できます。

mobileconfigですが実態は下のようなXMLファイルです。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>PayloadContent</key>
    <array>
        <dict>
            <key>FullScreen</key>
            <false/>
            <key>Icon</key>
            <data>
            iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAIAAAB7GkOtAAAAAXNS
            省略…
            gOYGkdeNgH5AN0fo+SdwALUIFMO12gNvAAAAAElFTkSuQmCC
            </data>
            <key>IgnoreManifestScope</key>
            <false/>
            <key>IsRemovable</key>
            <true/>
            <key>Label</key>
            <string>WEBさいと</string>
            <key>PayloadDescription</key>
            <string>Webクリップの設定を構成します</string>
            <key>PayloadDisplayName</key>
            <string>Webクリップ</string>
            <key>PayloadIdentifier</key>
            <string>com.apple.webClip.managed.XXXXXXXX</string>
            <key>PayloadType</key>
            <string>com.apple.webClip.managed</string>
            <key>PayloadUUID</key>
            <string>XXXXXXXX</string>
            <key>PayloadVersion</key>
            <integer>1</integer>
            <key>Precomposed</key>
            <false/>
            <key>URL</key>
            <string>https://google.co.jp</string>
        </dict>
    </array>
    <key>PayloadDisplayName</key>
    <string>名称未設定</string>
    <key>PayloadIdentifier</key>
    <string>xxxxx</string>
    <key>PayloadRemovalDisallowed</key>
    <false/>
    <key>PayloadType</key>
    <string>Configuration</string>
    <key>PayloadUUID</key>
    <string>XXXXXXX</string>
    <key>PayloadVersion</key>
    <integer>1</integer>
</dict>
</plist>

mobileconfigファイルを作ったらどこかにアップロードしてiPhoneにダウンロードします。
ダウンロード後は設定アプリからプロファイルのインストールを行います。

インストールするとホーム画面に先程作ったアイコンが表示されます。
タップすると先程設定したURLに遷移します。

プロファイルを削除すれば先程作ったアイコンも消えます。

ActionProviderでサブメニューを表示する

ActionProviderというクラスを使ってAndroidアプリのメニューにサブメニューを入れてみました。

Activityでメニューを表示して動かするためには下のようなメソッドを実装します。

class MainActivity : AppCompatActivity() {
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.action_settings -> true
            else -> super.onOptionsItemSelected(item)
        }
    }
}

onCreateOptionsMenuで呼び出しているmenu_main.xmlは下のようになっています。

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.example.myapplication.MainActivity">
    <item
        android:id="@+id/action_settings"
        android:orderInCategory="100"
        android:title="@string/action_settings"
        app:showAsAction="never" />
</menu>

アプリを起動すると右上にメニュー(…表示)が出ます。
ここをタップするとmenu_main.xmlの内容が表示されます。

今回はActionProviderを使ってメニューのサブメニュー部分を作ってみました。
まずはサブメニュー用にmenu_test.xmlを作成します。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:title="Menu1" />
    <item android:title="Menu2" />
    <item android:title="Menu3" />
</menu>

次にActionProviderを継承したクラスを作ります。
hasSubMenuとonPrepareSubMenuでサブメニューの設定をしています。
onPerformDefaultActionはonOptionsItemSelectedが実装されていない時に呼ばれるメソッドです。

class MyActionProvider(private val context: Context): ActionProvider(context) {
    override fun onCreateActionView(): View {
        return View(context)
    }

    override fun onPerformDefaultAction(): Boolean {
        Log.e("Test", "test")
        return super.onPerformDefaultAction()
    }

    override fun hasSubMenu(): Boolean {
        return true
    }

    override fun onPrepareSubMenu(subMenu: SubMenu) {
        subMenu.clear()
        MenuInflater(context).inflate(R.menu.menu_test, subMenu)
    }
}

最後にmenu_main.xmlでactionProviderClassを使ってActionProviderを設定します。

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.example.myapplication.MainActivity">
    <item
        android:id="@+id/action_settings"
        android:title="@string/action_settings"
        app:actionProviderClass="com.example.myapplication.MyActionProvider" />
</menu>

これでメニューを選んだ時、サブメニューが出るようになりました。

CoordinatorLayout+AppBarLayoutで動的なツールバーを作る

CoordinatorLayoutとAppBarLayoutを使ってスクロールすると自動で畳まれるツールバーを作ってみました。

まずはスクロールのテスト用にRecyclerViewを表示します。
activity_main.xmlを下のように変更します。

layout_behaviorはツールバー これがない場合、ツールバーとRecyclerViewが重なって表示されてしまいます。

layout_behaviorはRecyclerViewのレイアウトに関するプロパティーです。
これがない場合、ツールバーとRecyclerViewが重なって表示されてしまいます。

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

次はRecyclerViewのコンテンツをセットします。
MainActivityを変更します。

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.recyclerView.apply {
            layoutManager = LinearLayoutManager(this@MainActivity)

            val texts = (0..20).map { "test$it" }
            adapter = MyAdapter(texts)
        }
    }
}

class MyAdapter(private val texts: List<String>): RecyclerView.Adapter<MyAdapter.ViewHolder>() {
    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val textView: TextView

        init {
            textView = view.findViewById(R.id.my_text_view)
        }
    }

    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
        val itemView = LayoutInflater.from(viewGroup.context)
            .inflate(R.layout.row_main_activity, viewGroup, false)
        return ViewHolder(itemView)
    }

    override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
        viewHolder.textView.text = texts[position]
    }

    override fun getItemCount() = texts.count()
}

これでRecyclerViewが表示されました。

次はツールバーを表示します。
activity_main.xmlのRecyclerViewの上に下のコードを追加します。
今回はツールバーの上にボタンを表示するようにしました。

<com.google.android.material.appbar.AppBarLayout
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:theme="@style/Theme.MyApplication.AppBarOverlay">

    <com.google.android.material.appbar.CollapsingToolbarLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:minHeight="?attr/actionBarSize"
        app:layout_scrollFlags="scroll|exitUntilCollapsed">

        <Button
            android:layout_width="match_parent"
            android:layout_height="44dp"
            android:text="test" />

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/Theme.MyApplication.PopupOverlay" />
    </com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>

ツールバーを表示するためにMainActivityのonCreateに下の処理も追加します。

setSupportActionBar(binding.toolbar)

アプリを起動すると下のようにツールバーの上にボタンが表示されます。

RecyclerViewをスクロールすると下のように折りたたまれます。

CollapsingToolbarLayoutのlayout_scrollFlagsを変える事でスクロール時の処理を変える事が可能です。
例えばlayout_scrollFlagsをscroll|enterAlwaysCollapsedに変えると、スクロール時にツールバーがすべて隠れるようになります。

Kotlinの::構文について調べる

Kotlinに時々出てくる::構文について調べてみました。

::ですが下のようにメソッドを変数化する時に利用します。

val myMethod = ::test
myMethod()
myMethod()

fun test() {
    Log.e("test", "TEST")
}

他にはクラス情報を取得する時にも利用します。
クラス情報はIntentの引数などに使います。

val myClass = MainActivity::class.java

val intent = Intent(context, myClass)

下のようにbuild.gradleにkotlin-reflectを入れればクラスのメソッドやプロパティー一覧にアクセスする事ができます。

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-reflect:1.7.0"
}

アクセス方法は下のとおりです。

val myClass = MainActivity::class
myClass.members.forEach {
    Log.e("test", "${it.name}")
}

下のようにプロパティーの情報を取得する事も可能です。
以下ではプロパティーの戻り値の型を取得しています。

class MainActivity : AppCompatActivity() {
    var test = ""

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val p = ::test
        Log.e("test", "${p.returnType}") // → kotlin.String
    }
}

上の方法を使ってlateinitのプロパティーの初期化がされているかを調べる事もできます。

class MainActivity : AppCompatActivity() {
    lateinit var test: String

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val isInitialized = ::test.isInitialized
    }
}

この方法はlateinit以外の変数で使おうとするとコンパイルエラーになります。

class MainActivity : AppCompatActivity() {
    var test: String = ""

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val isInitialized = ::test.isInitialized
    }
}

参考URL

Kotlinの.isInitialized呼び出しになぜCallableReferenceの指定が必要なのか | thara.dev

Androidのコンテンツプロバイダを試してみる

コンテンツプロバイダを使ってデータの保存を試してみました。
コンテンツプロバイダとはAndroidの用意しているデータ保存の仕組みで、他アプリとのデータ共有を簡単にできるようになります。
今回はこれを使ってデータの保存をしてみようと思います。

まずはプロジェクトを作成します。

次は自前のContentProviderクラスを作成します。
onCreate、query、insert、update、delete、getTypeは必須のメソッドになります。

class MyProvider: ContentProvider() {
    override fun onCreate(): Boolean {
        return true
    }

    override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? {
        return null
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        return null
    }

    override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {
        return 0
    }

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
        return 0
    }

    override fun getType(uri: Uri): String? {
        return null
    }
}

次はAndroidManifest.xmlのapplicationの中に下のような宣言をします。
こうする事でcom.exampleのURLの時にMyProviderが呼び出されます。

<provider
    android:name=".MyProvider"
    android:authorities="com.example" />

あとはMainActivityなどで下のようにcontentResolverを使ってinsertを呼ぶとMyProviderのinsertが呼ばれるようになります。

val uri = Uri.parse("content://com.example/")
contentResolver.insert(uri, ContentValues().apply {
    put("name", "name1")
})

次は実際にデータの取得・保存・編集・削除をできるようにします。
今回はSQLiteを使ったデータの保存を行います。

まずは下のようにSQLiteにアクセスするためのクラスを作成します。
onCreate内でTableの作成をしています。

class MyDbHelper(context: Context): SQLiteOpenHelper(context, "my_database.db", null, 1) {
    override fun onCreate(db: SQLiteDatabase?) {
        db?.execSQL("create table my_database (_id integer primary key autoincrement, name text not null);")
    }

    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
    }
}

次はContentProviderのonCreate、query、insertを実装します。

class MyProvider: ContentProvider() {
    lateinit var helper: MyDbHelper

    override fun onCreate(): Boolean {
        helper = MyDbHelper(context!!)
        return true
    }

    override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? {
        return helper.readableDatabase.query("my_database", arrayOf("name"), selection, selectionArgs, null, null, null)
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        val id = helper.writableDatabase.insert("my_database", null, values)
        return ContentUris.withAppendedId(uri, id)
    }
}

MainActivityなどで保存・取得処理を動かすと無事にデータの保存ができている事が分かります。

contentResolver.insert(uri, ContentValues().apply {
    put("name", "name1")
})
contentResolver.query(uri, null, null, null, null)?.apply {
    moveToFirst()
    while (moveToNext()) {
        Log.e("Test", getString(0)) // → name1
    }
}

次は他アプリでこのデータが取得できるかを確認します。
まずはAndroidManifest.xmlのproviderにandroid:exportedを付けます。
こうすることで外部からのアクセスができるようになります。

<provider
    android:name=".MyProvider"
    android:authorities="com.example"
    android:exported="true" />

付けない場合は下のようなエラーが発生します。

Permission Denial: opening provider com.example.myapplication.MyProvider from ProcessRecord{com.example.myapplication2} (pid=xxx, uid=xxx) that is not exported from UID xxx

次に新規プロジェクトを作成します。

あとは下のように同一URLで取得するだけです。
AndroidManifest.xmlの変更やContentProviderのサブクラスを作る必要はありません。

val uri = Uri.parse("content://com.example/")
contentResolver.query(uri, null, null, null, null)?.apply {
    moveToFirst()
    while (moveToNext()) {
        Log.e("Test", getString(0)) // → name1
    }
}

今回はやらないのですが、更新・削除もupdate・deleteメソッドの実装をする事で実現できそうです。

ちなみにデータは元のアプリケーション内にあるので、元アプリケーションを削除すると一緒に消えます。
元アプリケーションがない場合、contentResolverのqueryを呼び出してもnullが返ってくるようになります。

それと両方のアプリのAndroidManifest.xmlで同じauthoritiesのProviderを作ると下のようなエラーになるのでURLが被らないよう注意する必要があります。

Installation failed due to: 'INSTALL_FAILED_CONFLICTING_PROVIDER: Package couldn't be installed in /data/app/com.example.myapplication2-xxx==: Can't install because provider name com.example (in package com.example.myapplication2) is already used by com.example.myapplication'

Google App Engine上でRailsを動かしてみる

タイトルの通り、Google App Engine上でRailsを動かしてみました。
ドキュメントは下を参考にしています。

App Engine フレキシブル環境で Rails 5 を実行する  |  Ruby  |  Google Cloud

まずはRailsプロジェクトを作成します。

rails new test_app

次にAppEngineの設定をします。
プロジェクトのルートにapp.yamlを作成して下のように設定します。
[SECRET_KEY]の部分はrails secretコマンドで作った秘密鍵に置き換えます。

entrypoint: bundle exec rackup --port $PORT
env: flex
runtime: ruby

env_variables:
  SECRET_KEY_BASE: [SECRET_KEY]

次は下コマンドGoogleCloudSDKをインストールします。

brew install --cask google-cloud-sdk

インストールが終わったら下コマンドでログインします。

gcloud auth login

ログインが終わったらGoogle Cloudのページで新規にプロジェクトを作成します。

Google Cloud Platform

プロジェクトを作ったら下コマンドで先程作ったプロジェクトを選択します。

gcloud config set project [先程作成したプロジェクト名]

プロジェクトのセットをしたら下コマンドでアプリを作成します。

gcloud app create

アプリの場所を選ぶように言われるので好きな場所を選択します。

最後に下コマンドをデプロイ完了です。
デプロイをするためにはプロジェクトに請求情報を紐付ける必要があるので注意が必要です。

gcloud app deploy

Android Studioのプロファイラでボトルネックを探してみる

Android Studioのプロファイラを使ってAndroidアプリのどこが重いかの測定をしてみました。

まずは新規プロジェクトを作成します。

プロファイラはメニューのViewから起動します。

起動すると下のようなViewが表示されます。

アプリを起動すると自動的に接続されます。

記録はViewの左側のRecordボタンから開始します。

Stopを押せば記録は完了して下のような表示になります。

重い処理を作るため、FirstFragmentのsetOnClickListenerを直してみます。

class FirstFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.buttonFirst.setOnClickListener {
            (0..1_000_000_000).forEach {
                val t = 100 * 100
            }
            findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
        }
    }
}

測定完了後、Flame Chartを開くと下のような表示になります。
マウスを当てるとクリックしたタイミングで4秒ほどかかっている事が分かりました。

Bottom Up画面でも重い処理を見つける事ができました。

ただ、不慣れなせいか重い箇所をピンポイントで特定するのは時間がかかりそうでした。
とりあえずしばらくは色々と試してみようと思います。