Androidでサービスが実行されているかどうかを確認するにはどうすればよいですか?

StackOverflow https://stackoverflow.com/questions/600207

  •  03-07-2019
  •  | 
  •  

質問

バックグラウンドサービスが実行されているかどうかを確認するにはどうすればよいですか

サービスの状態を切り替えるAndroidアクティビティが必要です。サービスがオフの場合はオンにし、オンの場合はオフにすることができます。

役に立ちましたか?

解決

私はずっと前に同じ問題を抱えていました。私のサービスはローカルだったため、hackbod こちら

編集(レコード用):

hackbodが提案するソリューションは次のとおりです。

  

クライアントとサーバーのコードが同じ.apkの一部であり、あなたが   具体的なインテント(サービスを指定するもの)を使用してサービスにバインドする   正確なサービスクラス)、その後、単純にサービスを設定することができます   クライアントがチェックできる実行中のグローバル変数。

     

意図的にサービスがあるかどうかを確認するAPIはありません   ほぼ間違いなく、あなたが何かをしたいとき   コード内で競合状態になってしまうようなものです。

他のヒント

アクティビティ内から次を使用します:

private boolean isMyServiceRunning(Class<?> serviceClass) {
    ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
    for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
        if (serviceClass.getName().equals(service.service.getClassName())) {
            return true;
        }
    }
    return false;
}

そして、私はそれを使用して呼び出します:

isMyServiceRunning(MyService.class)

これは、 ActivityManager#getRunningServices

onDestroyまたはonSometingイベント、バインダー、または静的変数を使用するすべてのアプローチは、Androidがプロセスを強制終了するか、言及されたコールバックのどれを呼び出すかを開発者が知らないため、確実に機能しません。 「killable」に注意してください。 Androidのドキュメントのライフサイクルイベントの表の列。

了解!

サービスを適切に登録するには MUST を呼び出し startService()し、 BIND_AUTO_CREATE を渡すだけでは不十分です。

Intent bindIntent = new Intent(this,ServiceTask.class);
startService(bindIntent);
bindService(bindIntent,mConnection,0);

そしてServiceToolsクラス:

public class ServiceTools {
    private static String LOG_TAG = ServiceTools.class.getName();

    public static boolean isServiceRunning(String serviceClassName){
        final ActivityManager activityManager = (ActivityManager)Application.getContext().getSystemService(Context.ACTIVITY_SERVICE);
        final List<RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE);

        for (RunningServiceInfo runningServiceInfo : services) {
            if (runningServiceInfo.service.getClassName().equals(serviceClassName)){
                return true;
            }
        }
        return false;
     }
}

小さな補足は:

私の目標は、サービスが実行されていない場合、実際に実行せずにサービスが実行されているかどうかを知ることです。

bindServiceを呼び出すか、サービスでキャッチできるインテントを呼び出すことは、サービスが実行されていない場合にサービスを開始するため、お勧めできません。

したがって、miracle2kが示唆したように、サービスが開始されたかどうかを知るために、サービスクラスに静的フィールドを持つことが最善です。

さらにクリーンにするために、非常に遅延フェッチを使用してシングルトンにサービスを変換することをお勧めします。つまり、シングルトンインスタンスを静的メソッド経由で。サービス/シングルトンの静的getInstanceメソッドは、シングルトンのインスタンスが作成されている場合、そのインスタンスを返します。しかし、実際にはシングルトン自体を開始したりインスタンス化したりするわけではありません。サービスは通常のサービス開始方法でのみ開始されます。

シングルトンデザインパターンを変更して、混乱するgetInstanceメソッドの名前を isInstanceCreated():boolean メソッドのような名前に変更すると、さらにクリーンになります。

コードは次のようになります。

public class MyService extends Service
{
   private static MyService instance = null;

   public static boolean isInstanceCreated() {
      return instance != null;
   }//met

   @Override
   public void onCreate()
   {
      instance = this;
      ....
   }//met

   @Override
   public void onDestroy()
   {
      instance = null;
      ...
   }//met
}//class

このソリューションはエレガントですが、サービスクラスへのアクセス権があり、サービスのアプリ/パッケージのクラスにのみアクセスできる場合にのみ関連します。クラスがサービスアプリ/パッケージの外部にある場合、Pieter-Jan Van Robaysが強調した制限付きでActivityManagerをクエリできます。

これを使用できます(まだ試していませんが、うまくいくと思います):

if(startService(someIntent) != null) {
    Toast.makeText(getBaseContext(), "Service is already running", Toast.LENGTH_SHORT).show();
}
else {
    Toast.makeText(getBaseContext(), "There is no service running, starting service..", Toast.LENGTH_SHORT).show();
}

すでに実行中のサービスがある場合、startServiceメソッドはComponentNameオブジェクトを返します。そうでない場合、nullが返されます。

public abstractを参照ComponentName startService(インテントサービス)

これはサービスを開始するため、チェックとは異なります。コードの下に stopService(someIntent); を追加できます。

    public boolean checkServiceRunning(){
         ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
        for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) 
                {
                    if ("com.example.yourpackagename.YourServiceName"
                            .equals(service.service.getClassName())) 
                    {
                        return true;
                    }
                }
             return false;
    }

上記のソリューションの1つをわずかに変更しましたが、同じメソッド class.getName()

public class ServiceTools {
    private static String LOG_TAG = ServiceTools.class.getName();

    public static boolean isServiceRunning(Context context,Class<?> serviceClass){
        final ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);
        final List<RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE);

        for (RunningServiceInfo runningServiceInfo : services) {
            Log.d(Constants.TAG, String.format("Service:%s", runningServiceInfo.service.getClassName()));
            if (runningServiceInfo.service.getClassName().equals(serviceClass.getName())){
                return true;
            }
        }
        return false;
    }
}

そして

Boolean isServiceRunning = ServiceTools.isServiceRunning(
                    MainActivity.this.getApplicationContext(),
                    BackgroundIntentService.class);

Android ドキュメントからの抜粋:

  

sendBroadcast(Intent )、ただし、受信者がいる場合   この関数がブロックし、すぐにディスパッチするインテント   戻る前。

このハックを「ping」と考えてください。 Service 。同期的にブロードキャストできるため、UIスレッドでブロードキャストして結果を取得できます。

サービス

@Override
public void onCreate() {
   LocalBroadcastManager
     .getInstance(this)
     .registerReceiver(new ServiceEchoReceiver(), new IntentFilter("ping"));
}

private class ServiceEchoReceiver extends BroadcastReceiver {
    public void onReceive (Context context, Intent intent) {
      LocalBroadcastManager
         .getInstance(this)
         .sendBroadcastSync(new Intent("pong"));
    }
}

アクティビティ

    bool serviceRunning = false;

    protected void onCreate (Bundle savedInstanceState){
        LocalBroadcastManager.getInstance(this).registerReceiver(pong, new IntentFilter("pong"));
        LocalBroadcastManager.getInstance(this).sendBroadcastSync(new Intent("ping"));
        if(!serviceRunning){
           //run the service
        }
    }

    private BroadcastReceiver pong = new BroadcastReceiver(){
        public void onReceive (Context context, Intent intent) {
          serviceRunning = true;   
        }
    }

@Snicolasの回答にメモを追加したいだけです。次の手順を使用して、 onDestroy()の呼び出しの有無にかかわらず、停止サービスを確認できます。

  1. onDestroy()の呼び出し:設定に移動-&gt;アプリケーション-&gt;サービスの実行-&gt;サービスを選択して停止します。

  2. onDestroy()が呼び出されない:[設定]に移動-&gt;アプリケーション-&gt;アプリケーションの管理-&gt;選択して「強制停止」を選択しますサービスが実行されているアプリケーション。ただし、アプリケーションがここで停止されると、間違いなくサービスインスタンスも停止されます。

最後に、シングルトンクラスで静的変数を使用してそこで言及されているアプローチが機能していることに言及したいと思います。

onDestroy は常にサービスで呼び出されるとは限らないため、これは役に立ちません!

例:Eclipseから1回変更するだけで、アプリを再度実行します。アプリケーションはSIGを使用して強制的に終了します:9。

サービスが実行されているかどうかを確認する適切な方法は、単にそれを尋ねることです。アクティビティからのpingに応答するBroadcastReceiverをサービスに実装します。サービスの開始時にBroadcastReceiverを登録し、サービスが破棄されたときに登録を解除します。アクティビティ(またはコンポーネント)から、ローカルブロードキャストサービスへの意図。それが応答する場合、サービスが実行されていることがわかります。以下のコードのACTION_PINGとACTION_PONGの微妙な違いに注意してください。

public class PingableService extends Service
{
    public static final String ACTION_PING = PingableService.class.getName() + ".PING";
    public static final String ACTION_PONG = PingableService.class.getName() + ".PONG";

    public int onStartCommand (Intent intent, int flags, int startId)
    {
        LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, new IntentFilter(ACTION_PING));
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy ()
    {
        LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
        super.onDestroy();
    }

    private BroadcastReceiver mReceiver = new BroadcastReceiver()
    {
        @Override
        public void onReceive (Context context, Intent intent)
        {
            if (intent.getAction().equals(ACTION_PING))
            {
                LocalBroadcastManager manager = LocalBroadcastManager.getInstance(getApplicationContext());
                manager.sendBroadcast(new Intent(ACTION_PONG));
            }
        }
    };
}


public class MyActivity extends Activity
{
    private boolean isSvcRunning = false;

    @Override
    protected void onStart()
    {
        LocalBroadcastManager manager = LocalBroadcastManager.getInstance(getApplicationContext());
        manager.registerReceiver(mReceiver, new IntentFilter(PingableService.ACTION_PONG));
        // the service will respond to this broadcast only if it's running
        manager.sendBroadcast(new Intent(PingableService.ACTION_PING));
        super.onStart();
    }

    @Override
    protected void onStop()
    {
        LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
        super.onStop();
    }

    protected BroadcastReceiver mReceiver = new BroadcastReceiver()
    {
        @Override
        public void onReceive (Context context, Intent intent)
        {
            // here you receive the response from the service
            if (intent.getAction().equals(PingableService.ACTION_PONG))
            {
                isSvcRunning = true;
            }
        }
    };
}

まず、ActivityManagerを使用してサービスにアクセスしようとしないでください。 (こちら

サービスは、単独で実行することも、アクティビティにバインドすることも、その両方を実行することもできます。サービスが実行中かどうかをアクティビティでチェックインする方法は、アクティビティとサービスの両方が理解できるメソッドを宣言するインターフェース(バインダーを拡張する)を作成することです。これを行うには、たとえば&quot; isServiceRunning()&quot;を宣言する独自のインターフェイスを作成します。 その後、アクティビティをサービスにバインドし、メソッドisServiceRunning()を実行します。サービスは実行中かどうかをチェックし、アクティビティにブール値を返します。

このメソッドを使用して、サービスを停止したり、別の方法でサービスとやり取りしたりすることもできます。

これを使用しましたチュートリアルこのシナリオをアプリケーションに実装する方法を学習します。

Xamarin C#バージョン:

private bool isMyServiceRunning(System.Type cls)
{
    ActivityManager manager = (ActivityManager)GetSystemService(Context.ActivityService);

    foreach (var service in manager.GetRunningServices(int.MaxValue)) {
        if (service.Service.ClassName.Equals(Java.Lang.Class.FromType(cls).CanonicalName)) {
            return true;
        }
    }
    return false;
}

ここで示したユースケースでは、単に stopService()メソッドの戻り値を使用できます。指定されたサービスが存在し、強制終了された場合、 true を返します。それ以外の場合は、 false を返します。したがって、結果が false の場合はサービスを再起動できます。そうでない場合は、現在のサービスが停止していることが保証されます。 :) これ

また、人々が保留中のインテントを使用するとよりクリーンになる可能性のある別の代替手段(たとえば、 AlarmManager

public static boolean isRunning(Class<? extends Service> serviceClass) {
    final Intent intent = new Intent(context, serviceClass);
    return (PendingIntent.getService(context, CODE, intent, PendingIntent.FLAG_NO_CREATE) != null);
}

CODE は、サービスに関連付けられた保留中のインテントを識別するためにクラスでプライベートに定義する定数です。

Belowは、すべての Ifs を網羅するエレガントなハックです。これはローカルサービス専用です。

    public final class AService extends Service {

        private static AService mInstance = null;

        public static boolean isServiceCreated() {
            try {
                // If instance was not cleared but the service was destroyed an Exception will be thrown
                return mInstance != null && mInstance.ping();
            } catch (NullPointerException e) {
                // destroyed/not-started
                return false;
            }
        }

        /**
         * Simply returns true. If the service is still active, this method will be accessible.
         * @return
         */
        private boolean ping() {
            return true;
        }

        @Override
        public void onCreate() {
            mInstance = this;
        }

        @Override
        public void onDestroy() {
            mInstance = null;
        }
    }

そして後で:

    if(AService.isServiceCreated()){
        ...
    }else{
        startService(...);
    }
  

geekQの応答ですが、Kotlinクラスです。ありがとうgeekQ

fun isMyServiceRunning(serviceClass : Class<*> ) : Boolean{
    var manager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    for (service in manager.getRunningServices(Integer.MAX_VALUE)) {
        if (serviceClass.name.equals(service.service.className)) {
            return true
        }
    }
    return false
}

呼び出し

isMyServiceRunning(NewService::class.java)

同じクラス名を持つ複数のサービスが存在する場合があります。

2つのアプリを作成しました。最初のアプリのパッケージ名は com.example.mock です。アプリで lorem というサブパッケージと、 Mock2Service というサービスを作成しました。そのため、完全修飾名は com.example.mock.lorem.Mock2Service です。

次に、2番目のアプリと Mock2Service というサービスを作成しました。 2番目のアプリのパッケージ名は com.example.mock.lorem です。サービスの完全修飾名も com.example.mock.lorem.Mock2Service です。

ここに私のlogcat出力があります。

03-27 12:02:19.985: D/TAG(32155): Mock-01: com.example.mock.lorem.Mock2Service
03-27 12:02:33.755: D/TAG(32277): Mock-02: com.example.mock.lorem.Mock2Service

ComponentName equals()はパッケージ名とクラス名の両方を比較するため、 ComponentName インスタンスを比較することをお勧めします。また、同じパッケージ名を持つ2つのアプリをデバイスにインストールすることはできません。

ComponentName のequals()メソッド。

@Override
public boolean equals(Object obj) {
    try {
        if (obj != null) {
            ComponentName other = (ComponentName)obj;
            // Note: no null checks, because mPackage and mClass can
            // never be null.
            return mPackage.equals(other.mPackage)
                    && mClass.equals(other.mClass);
        }
    } catch (ClassCastException e) {
    }
    return false;
}

ComponentName

これはスレッドを生成するため、Intent Serviceのデバッグに適用されますが、通常のサービスでも機能する場合があります。このスレッドはBingingのおかげで見つかりました

私の場合、デバッガをいじってみたところ、スレッドビューが見つかりました。 MS Wordの箇条書きアイコンのように見えます。とにかく、それを使用するためにデバッガーモードにある必要はありません。プロセスをクリックして、そのボタンをクリックします。少なくともエミュレータ上では、実行中のIntentサービスが表示されます。

サービスが別のプロセスまたはAPKに属している場合、ActivityManagerに基づいたソリューションを使用します。

ソースにアクセスできる場合は、静的フィールドに基づいたソリューションを使用してください。しかし、代わりにブール値を使用して、Dateオブジェクトを使用することをお勧めします。サービスの実行中に、その値を「now」に更新し、終了したらnullに設定するだけです。アクティビティから、nullまたは日付が古すぎて実行されていないことを確認できます。

サービスからブロードキャスト通知を送信して、進行状況などの詳細情報に沿って実行されていることを示すこともできます。

TheServiceClassの内部で定義します:

 public static Boolean serviceRunning = false;

onStartCommand(...)で

 public int onStartCommand(Intent intent, int flags, int startId) {

    serviceRunning = true;
    ...
}

 @Override
public void onDestroy()
{
    serviceRunning = false;

} 

次に、任意のクラスから if(TheServiceClass.serviceRunning == true)を呼び出します。

バインドを使用せずにバインドを自動作成しない -psを参照してください。そして更新...

public abstract class Context {

 ... 

  /*
  * @return {true} If you have successfully bound to the service, 
  *  {false} is returned if the connection is not made 
  *  so you will not receive the service object.
  */
  public abstract boolean bindService(@RequiresPermission Intent service,
        @NonNull ServiceConnection conn, @BindServiceFlags int flags);

例:

    Intent bindIntent = new Intent(context, Class<Service>);
    boolean bindResult = context.bindService(bindIntent, ServiceConnection, 0);

使用しない理由 getRunningServices()

List<ActivityManager.RunningServiceInfo> getRunningServices (int maxNum)
Return a list of the services that are currently running.

注:このメソッドは、サービス管理タイプのユーザーインターフェースのデバッグまたは実装のみを目的としています。


ps。アンドロイドのドキュメントは誤解を招きます。疑念を排除するためにGoogleトラッカーで問題を公開しました:

https://issuetracker.google.com/issues/68908332

バインドサービスが実際にサービスキャッシュバインダーを介してActivityManagerバインダーを介してトランザクションを呼び出すのを見ることができます-バインドの結果を確認できるように、バインドの結果は次のとおりです:

int res = ActivityManagerNative.getDefault().bindService(...);
return res != 0;

取引はバインダーを介して行われます:

ServiceManager.getService("activity");

次:

  public static IBinder getService(String name) {
    try {
        IBinder service = sCache.get(name);
        if (service != null) {
            return service;
        } else {
            return getIServiceManager().getService(name);

これはActivityThreadで次のように設定されます:

 public final void bindApplication(...) {

        if (services != null) {
            // Setup the service cache in the ServiceManager
            ServiceManager.initServiceCache(services);
        }

これは、メソッドのActivityManagerServiceで呼び出されます:

 private final boolean attachApplicationLocked(IApplicationThread thread,
            int pid) {
    ...
    thread.bindApplication(... , getCommonServicesLocked(),...)

then:

 private HashMap<String, IBinder> getCommonServicesLocked() {

ただし、「アクティビティ」はありません。ウィンドウパッケージとアラームのみ。

したがって、電話に戻る必要があります:

 return getIServiceManager().getService(name);

    sServiceManager = ServiceManagerNative.asInterface(BinderInternal.getContextObject());

これによりコールが行われます:

    mRemote.transact(GET_SERVICE_TRANSACTION, data, reply, 0);

次のようになります:

BinderInternal.getContextObject()

そしてこれはネイティブメソッドです...

  /**
     * Return the global "context object" of the system.  This is usually
     * an implementation of IServiceManager, which you can use to find
     * other services.
     */
    public static final native IBinder getContextObject();

cを掘る時間がないので、残りのコールを分析するまで、回答を一時停止します。

ただし、サービスが実行されているかどうかを確認するための最良の方法は、バインドを作成することです(バインドが作成されていない場合、サービスは存在しません)-バインドを介してサービスの状態を照会します(保存された内部フラグを使用して状態)。

2018年6月23日更新

iはそれらを興味深いと感じました:

/**
 * Provide a binder to an already-bound service.  This method is synchronous
 * and will not start the target service if it is not present, so it is safe
 * to call from {@link #onReceive}.
 *
 * For peekService() to return a non null {@link android.os.IBinder} interface
 * the service must have published it before. In other words some component
 * must have called {@link android.content.Context#bindService(Intent, ServiceConnection, int)} on it.
 *
 * @param myContext The Context that had been passed to {@link #onReceive(Context, Intent)}
 * @param service Identifies the already-bound service you wish to use. See
 * {@link android.content.Context#bindService(Intent, ServiceConnection, int)}
 * for more information.
 */
public IBinder peekService(Context myContext, Intent service) {
    IActivityManager am = ActivityManager.getService();
    IBinder binder = null;
    try {
        service.prepareToLeaveProcess(myContext);
        binder = am.peekService(service, service.resolveTypeIfNeeded(
                myContext.getContentResolver()), myContext.getOpPackageName());
    } catch (RemoteException e) {
    }
    return binder;
}

要するに:)

&quot;既にバインドされているサービスにバインダーを提供します。このメソッドは同期的であり、ターゲットサービスが存在しない場合、ターゲットサービスを開始しません。&quot;

public IBinder peekService(Intentサービス、String resolveType、                 String callingPackage)throws RemoteException;

  

*

public static IBinder peekService(IBinder remote, Intent service, String resolvedType)
             throws RemoteException {
    Parcel data = Parcel.obtain();
    Parcel reply = Parcel.obtain();
    data.writeInterfaceToken("android.app.IActivityManager");
    service.writeToParcel(data, 0);
    data.writeString(resolvedType);
    remote.transact(android.os.IBinder.FIRST_CALL_TRANSACTION+84, data, reply, 0);
    reply.readException();
    IBinder binder = reply.readStrongBinder();
    reply.recycle();
    data.recycle();
    return binder;
}
  

*

サービスサブクラスで、静的ブールを使用して、以下に示すようにサービスの状態を取得します。

MyService.kt

class MyService : Service() {
    override fun onCreate() {
        super.onCreate()
        isServiceStarted = true
    }
    override fun onDestroy() {
        super.onDestroy()
        isServiceStarted = false
    }
    companion object {
        var isServiceStarted = false
    }
}

MainActivity.kt

class MainActivity : AppCompatActivity(){
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val serviceStarted = FileObserverService.isServiceStarted
        if (!serviceStarted) {
            val startFileObserverService = Intent(this, FileObserverService::class.java)
            ContextCompat.startForegroundService(this, startFileObserverService)
        }
    }
}

ActivityManager :: getRunningServices ベースの回答のkotlin変換。この関数をアクティビティに追加します

private fun isMyServiceRunning(serviceClass: Class<out Service>) =
    (getSystemService(ACTIVITY_SERVICE) as ActivityManager)
        .getRunningServices(Int.MAX_VALUE)
        ?.map { it.service.className }
        ?.contains(serviceClass.name) ?: false

このコードを使用してください。

if (isMyServiceRunning(MainActivity.this, xyzService.class)) { // Service class name
    // Service running
} else {
    // Service Stop
}


public static boolean isMyServiceRunning(Activity activity, Class<?> serviceClass) {
        ActivityManager manager = (ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE);
        for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
            if (serviceClass.getName().equals(service.service.getClassName())) {
                return true;
            }
        }
        return false;
    }

簡単にやってみましょう...:)

最も適切なソリューションは、 SharedPreferences でサービスが実行されているかどうかについてのキーと値のペアを保持することだと思います。

論理は非常に単純です。サービスクラスの任意の位置で。サービスが実行中かどうかについてのフラグとして機能するブール値を入力します。次に、アプリケーション内の任意の場所でこの値を読み取ります。

アプリで使用しているサンプルコードは次のとおりです。

サービスクラス(オーディオストリームのサービス)では、サービスが起動しているときに次のコードを実行します。

private void updatePlayerStatus(boolean isRadioPlaying)
{
        SharedPreferences sharedPref = this.getSharedPreferences(getString(R.string.str_shared_file_name), Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sharedPref.edit();
        editor.putBoolean(getString(R.string.str_shared_file_radio_status_key), isRadioPlaying);
        editor.commit();
}

アプリケーションのアクティビティで、次のコードを使用してサービスのステータスを確認しています。

private boolean isRadioRunning() {
        SharedPreferences sharedPref = this.getSharedPreferences(getString(R.string.str_shared_file_name), Context.MODE_PRIVATE);

        return sharedPref.getBoolean(getString(R.string.str_shared_file_radio_status_key), false);
}

特別な権限やループはありません...簡単な方法、クリーンなソリューション:)

追加情報が必要な場合は、リンク

これがお役に立てば幸いです。

scroll top