/* DConnectLaunchActivity.java Copyright (c) 2014 NTT DOCOMO,INC. Released under the MIT license http://opensource.org/licenses/mit-license.php */ package org.deviceconnect.android.manager; import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.text.TextUtils; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.widget.Button; import android.widget.TextView; import org.deviceconnect.android.manager.hmac.HmacManager; import org.deviceconnect.android.manager.setting.SettingActivity; import org.deviceconnect.message.intent.message.IntentDConnectMessage; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.logging.Logger; /** * Device Connect Manager Launch Activity. * * @author NTT DOCOMO, INC. */ public class DConnectLaunchActivity extends Activity { /** * The names of URI scheme for launching Device Connect Manager. */ private static final String[] SCHEMES_LAUNCH = {"dconnect", "gotapi"}; private static final String HOST_START = "start"; private static final String HOST_STOP = "stop"; private static final String PATH_ROOT = "/"; private static final String PATH_ACTIVITY = PATH_ROOT + "activity"; private static final String PATH_SERVER = PATH_ROOT + "server"; private static final int RESULT_ERROR = Activity.RESULT_FIRST_USER; /** * The HMAC manager. */ private HmacManager mHmacManager; /** * DConnectServiceを操作するクラス. */ private DConnectService mDConnectService; /** * The logger. */ protected final Logger mLogger = Logger.getLogger("dconnect.manager"); private DConnectSettings mSettings = DConnectSettings.getInstance(); private Runnable mBehavior; /** マネージャ本体のサービスがBindされているかどうか. */ private boolean mIsBind = false; @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_ACTION_BAR); getActionBar().hide(); mHmacManager = new HmacManager(this); mSettings.load(this); processRequest(getIntent()); } private boolean allowExternalStartAndStop() { return mSettings.allowExternalStartAndStop(); } private void processRequest(final Intent intent) { if (intent != null && isSchemeForLaunch(intent.getScheme())) { updateHMACKey(intent); Uri uri = intent.getData(); String host = uri.getHost(); String path = uri.getPath(); if (HOST_START.equals(host)) { preventAutoStop(); if (!allowExternalStartAndStop() || PATH_ROOT.equals(path) || PATH_ACTIVITY.equals(path)) { mBehavior = new Runnable() { @Override public void run() { if (mDConnectService != null) { if (!mDConnectService.isRunning()) { displayActivity(); } else { finish(); } } else { finish(); } } }; } else if (PATH_SERVER.equals(path)) { mBehavior = new Runnable() { @Override public void run() { startManager(); setResult(RESULT_OK); finish(); } }; } else { finish(); return; } } else if (HOST_STOP.equals(host)) { if (!allowExternalStartAndStop() || PATH_ROOT.equals(path) || PATH_ACTIVITY.equals(path)) { mBehavior = new Runnable() { @Override public void run() { if (mDConnectService != null) { if (mDConnectService.isRunning()) { displayActivity(); } else { finish(); } } else { finish(); } } }; } else if (PATH_SERVER.equals(path)) { mBehavior = new Runnable() { @Override public void run() { boolean canStop = !existsConnectedWebSocket(); int result; if (canStop) { stopManager(); result = RESULT_OK; } else { mLogger.warning("Cannot stop Device Connect Manager automatically."); result = RESULT_ERROR; } setResult(result); finish(); } }; } else { finish(); return; } } bindManagerService(); } else { finish(); } } @Override protected void onNewIntent(final Intent intent) { setIntent(intent); } @Override protected void onPause() { super.onPause(); if (mIsBind) { unbindService(mServiceConnection); mIsBind = false; } finish(); } /** * Checks whether the specified scheme is for launching the Manager or not. * * @param receivedScheme the name of custom URI scheme received from an app. * @return <code>true</code> if the specified scheme is for launching the Manager, otherwise <code>false</code> */ private boolean isSchemeForLaunch(final String receivedScheme) { for (String scheme : SCHEMES_LAUNCH) { if (scheme.equals(receivedScheme)) { return true; } } return false; } private void updateHMACKey(final Intent intent) { Uri uri = intent.getData(); String key = intent.getStringExtra(IntentDConnectMessage.EXTRA_KEY); String origin = intent.getStringExtra(IntentDConnectMessage.EXTRA_ORIGIN); mLogger.info("Requested to update HMAC key from intent: origin=" + origin + ", key=" + key); if (key == null || origin == null) { mLogger.warning("Origin or key is missing."); key = uri.getQueryParameter(IntentDConnectMessage.EXTRA_KEY); origin = uri.getQueryParameter(IntentDConnectMessage.EXTRA_ORIGIN); mLogger.info("Requested to update HMAC key from URI: origin=" + origin + ", key=" + key); } try { if (origin != null) { origin = URLDecoder.decode(origin, "UTF-8"); if (key != null && !TextUtils.isEmpty(origin)) { mHmacManager.updateKey(origin, key); } } } catch (UnsupportedEncodingException e) { // nothing to do. mLogger.warning("Failed to decode origin=" + origin); } } private synchronized void bindManagerService() { Intent bindIntent = new Intent(getApplicationContext(), DConnectService.class); mIsBind = bindService(bindIntent, mServiceConnection, Context.BIND_AUTO_CREATE); } /** * DConnectServiceとのバインド解除時に、OSによってDConnectServiceが破棄されてしまうことを防ぐ. */ private void preventAutoStop() { Intent targetIntent = new Intent(); targetIntent.setClass(getApplicationContext(), DConnectService.class); startService(targetIntent); // NOTE: 上記の処理でstartServiceを実行している理由 // // AndroidフレームワークのServiceは、内部的に下記の2つのフラグを持つ. // // (1) startServiceされたことを示すフラグ // (2) bindServiceされたことを示すフラグ // // 上記のフラグをもとに、Android OSはServiceを破棄すべきかどうかを下記のように判断する. // // (1) のフラグのみがONの状態でstopServiceすると、そのServiceを破棄する. // (2) のフラグのみがONの状態でunbindServiceすると、そのServiceを破棄する. // (1)(2)両方がONの状態では、stopServiceとbindServiceの両方を実行した場合に限り、破棄する. // // 本画面の場合、画面が閉じられたタイミングでunbindServiceするため、bindServiceするだけでは // 画面を閉じられたタイミングでManagerのサービスが終了してしまう. // よって、startServiceも実行しておくことで、終了されてしまうことを回避する. } private void startManager() { if (mDConnectService != null) { mDConnectService.startInternal(); } } private void stopManager() { if (mDConnectService != null) { mDConnectService.stopInternal(); } } private void displayActivity() { getActionBar().show(); setContentView(R.layout.activity_dconnect_launcher); setTheme(R.style.AppTheme); View root = findViewById(R.id.launcher_root); root.setVisibility(View.VISIBLE); Button cancelButton = (Button) findViewById(R.id.button_manager_launcher_cancel); cancelButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { setResult(RESULT_OK); finish(); } }); if (mDConnectService != null) { toggleButton(mDConnectService.isRunning()); } } private boolean existsConnectedWebSocket() { WebSocketInfoManager mgr = ((DConnectApplication) getApplication()).getWebSocketInfoManager(); return mgr.getWebSocketInfos().size() > 0; } /** * Toggles the text on views. * * @param isLaunched <code>true</code> if Device Connect Manager is running, otherwise <code>false</code> */ private void toggleButton(final boolean isLaunched) { TextView messageView = (TextView) findViewById(R.id.text_manager_launcher_message); Button launchOrStopButton = (Button) findViewById(R.id.button_manager_launcher_launch_or_stop); if (messageView == null || launchOrStopButton == null) { return; } if (isLaunched) { messageView.setText(R.string.activity_launch_message_stop); launchOrStopButton.setText(R.string.activity_launch_button_stop); launchOrStopButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { stopManager(); setResult(RESULT_OK); finish(); } }); } else { messageView.setText(R.string.activity_launch_message_launch); launchOrStopButton.setText(R.string.activity_launch_button_launch); launchOrStopButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { startManager(); setResult(RESULT_OK); finish(); } }); } } @Override public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.activity_dconnect_launcher, menu); return true; } @Override public boolean onOptionsItemSelected(final MenuItem item) { int id = item.getItemId(); if (id == R.id.dconnect_launcher_menu_item_settings) { Intent intent = new Intent(this, SettingActivity.class); startActivity(intent); } else if (id == android.R.id.home) { onBackPressed(); } return true; } /** * DConnectServiceと接続するためのクラス. */ private final ServiceConnection mServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(final ComponentName name, final IBinder service) { mDConnectService = ((DConnectService.LocalBinder) service).getDConnectService(); runOnUiThread(new Runnable() { @Override public void run() { if (mBehavior != null) { mBehavior.run(); } } }); } @Override public void onServiceDisconnected(final ComponentName name) { mDConnectService = null; } }; }