/*
AuthorizationProfile.java
Copyright (c) 2014 NTT DOCOMO,INC.
Released under the MIT license
http://opensource.org/licenses/mit-license.php
*/
package org.deviceconnect.android.profile;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import org.deviceconnect.android.localoauth.AccessTokenData;
import org.deviceconnect.android.localoauth.AccessTokenScope;
import org.deviceconnect.android.localoauth.ClientData;
import org.deviceconnect.android.localoauth.ConfirmAuthParams;
import org.deviceconnect.android.localoauth.LocalOAuth2Main;
import org.deviceconnect.android.localoauth.PublishAccessTokenListener;
import org.deviceconnect.android.localoauth.exception.AuthorizationException;
import org.deviceconnect.android.message.DConnectMessageService;
import org.deviceconnect.android.message.MessageUtils;
import org.deviceconnect.android.profile.api.DConnectApi;
import org.deviceconnect.message.DConnectMessage;
import org.deviceconnect.profile.AuthorizationProfileConstants;
import org.restlet.ext.oauth.PackageInfoOAuth;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
/**
* Authorization プロファイル.
*
* <p>
* Local OAuthの認可機能を提供するAPI.<br>
* </p>
*
* @author NTT DOCOMO, INC.
*/
public class AuthorizationProfile extends DConnectProfile implements AuthorizationProfileConstants {
/** ロックオブジェクト. */
private final Object mLockObj = new Object();
/**
* プロファイルプロバイダー.
*/
private final DConnectProfileProvider mProvider;
/**
* 指定されたプロファイルプロバイダーをもつAuthorizationプロファイルを生成する.
*
* @param provider プロファイルプロバイダー
*/
public AuthorizationProfile(final DConnectProfileProvider provider) {
mProvider = provider;
addApi(mGrantApi);
addApi(mCreateAccessTokenApi);
}
/**
* デバイスプラグインのサポートするすべてのプロファイル名の配列を取得する.
* @return プロファイル名の配列
*/
private String[] getAllProfileNames() {
List<DConnectProfile> profiles = mProvider.getProfileList();
String[] names = new String[profiles.size()];
for (int i = 0; i < names.length; i++) {
names[i] = profiles.get(i).getProfileName();
}
return names;
}
@Override
public final String getProfileName() {
return PROFILE_NAME;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
// Local OAuthを使用しない場合にはNot Supportを返却する
DConnectMessageService service = (DConnectMessageService) getContext();
if (!service.isUseLocalOAuth()) {
MessageUtils.setNotSupportProfileError(response);
return true;
}
return super.onRequest(request, response);
}
/**
* Authorization Grant API.
*/
private final DConnectApi mGrantApi = new DConnectApi() {
@Override
public String getAttribute() {
return ATTRIBUTE_GRANT;
}
@Override
public Method getMethod() {
return Method.GET;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
new Thread(new Runnable() {
@Override
public void run() {
try {
createClient(request, response);
} catch (Exception e) {
MessageUtils.setAuthorizationError(response, e.getMessage());
}
sendResponse(response);
}
}).start();
return false;
}
};
/**
* Authorization Create Access Token API.
*/
private final DConnectApi mCreateAccessTokenApi = new DConnectApi() {
@Override
public String getAttribute() {
return ATTRIBUTE_ACCESS_TOKEN;
}
@Override
public Method getMethod() {
return Method.GET;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
new Thread(new Runnable() {
@Override
public void run() {
try {
getAccessToken(request, response);
} catch (AuthorizationException e) {
MessageUtils.setAuthorizationError(response, e.getMessage());
} catch (UnsupportedEncodingException e) {
MessageUtils.setInvalidRequestParameterError(response, e.getMessage());
} catch (IllegalArgumentException e) {
MessageUtils.setInvalidRequestParameterError(response, e.getMessage());
} catch (IllegalStateException e) {
MessageUtils.setInvalidRequestParameterError(response, e.getMessage());
} catch (Exception e) {
MessageUtils.setUnknownError(response, e.getMessage());
}
sendResponse(response);
}
}).start();
return false;
}
};
/**
* Clientデータを作成する.
*
* @param request リクエスト
* @param response レスポンス
*/
private void createClient(final Intent request, final Intent response) {
String packageName = request.getStringExtra(AuthorizationProfile.PARAM_PACKAGE);
String serviceId = request.getStringExtra(DConnectProfile.PARAM_SERVICE_ID);
if (packageName == null) {
MessageUtils.setInvalidRequestParameterError(response);
} else {
// Local OAuthでクライアント作成
PackageInfoOAuth packageInfo = new PackageInfoOAuth(packageName, serviceId);
try {
ClientData client = LocalOAuth2Main.createClient(packageInfo);
if (client != null) {
response.putExtra(DConnectMessage.EXTRA_RESULT, DConnectMessage.RESULT_OK);
response.putExtra(AuthorizationProfile.PARAM_CLIENT_ID, client.getClientId());
} else {
MessageUtils.setAuthorizationError(response, "Cannot create a client.");
}
} catch (AuthorizationException e) {
MessageUtils.setAuthorizationError(response, e.getMessage());
} catch (IllegalArgumentException e) {
MessageUtils.setInvalidRequestParameterError(response, e.getMessage());
}
}
}
/**
* アクセストークンの取得の処理を行う.
*
* @param request リクエスト
* @param response レスポンス
*
* @throws AuthorizationException 認証に失敗した場合に発生
* @throws UnsupportedEncodingException 文字のエンコードに失敗した場合に発生
*/
private void getAccessToken(final Intent request, final Intent response)
throws AuthorizationException, UnsupportedEncodingException {
String serviceId = request.getStringExtra(DConnectMessage.EXTRA_SERVICE_ID);
String clientId = request.getStringExtra(AuthorizationProfile.PARAM_CLIENT_ID);
String[] scopes = parseScopes(request.getStringExtra(AuthorizationProfile.PARAM_SCOPE));
if (scopes == null) {
scopes = getAllProfileNames();
}
String applicationName;
PackageManager pm = getContext().getPackageManager();
try {
PackageInfo info = pm.getPackageInfo(getContext().getPackageName(), 0);
ApplicationInfo ai = info.applicationInfo;
applicationName = (String) pm.getApplicationLabel(ai);
} catch (NameNotFoundException e) {
applicationName = request.getStringExtra(AuthorizationProfile.PARAM_APPLICATION_NAME);
}
// TODO _typeからアプリorデバイスプラグインかを判別できる?
ConfirmAuthParams params = new ConfirmAuthParams.Builder().context(getContext()).serviceId(serviceId)
.clientId(clientId).scopes(scopes).applicationName(applicationName)
.isForDevicePlugin(true)
.build();
// Local OAuthでAccessTokenを作成する。
final AccessTokenData[] token = new AccessTokenData[1];
LocalOAuth2Main.confirmPublishAccessToken(params, new PublishAccessTokenListener() {
@Override
public void onReceiveAccessToken(final AccessTokenData accessTokenData) {
token[0] = accessTokenData;
synchronized (mLockObj) {
mLockObj.notifyAll();
}
}
@Override
public void onReceiveException(final Exception exception) {
token[0] = null;
synchronized (mLockObj) {
mLockObj.notifyAll();
}
}
});
// ユーザからのレスポンスを待つ
if (token[0] == null) {
waitForResponse();
}
// アクセストークンの確認
if (token[0] != null && token[0].getAccessToken() != null) {
response.putExtra(DConnectMessage.EXTRA_RESULT, DConnectMessage.RESULT_OK);
response.putExtra(AuthorizationProfile.PARAM_ACCESS_TOKEN, token[0].getAccessToken());
AccessTokenScope[] atScopes = token[0].getScopes();
if (atScopes != null) {
List<Bundle> s = new ArrayList<>();
AccessTokenScope minScope = null;
for (AccessTokenScope scope : atScopes) {
Bundle b = new Bundle();
b.putString(PARAM_SCOPE, scope.getScope());
b.putLong(PARAM_EXPIRE_PERIOD, scope.getExpirePeriod());
s.add(b);
if (minScope == null || (minScope.getExpirePeriod() > scope.getExpirePeriod())) {
minScope = scope;
}
}
response.putExtra(PARAM_SCOPES, s.toArray(new Bundle[s.size()]));
// NOTE: GotAPI 1.0対応
if (minScope != null) {
response.putExtra(PARAM_EXPIRE, token[0].getTimestamp() + minScope.getExpirePeriod());
}
}
} else {
MessageUtils.setAuthorizationError(response, "Cannot create a access token.");
}
}
/**
* レスポンスが返ってくるまでの間スレッドを停止する.
* タイムアウトは設定していない。
*/
private void waitForResponse() {
synchronized (mLockObj) {
try {
mLockObj.wait();
} catch (InterruptedException e) {
mLogger.warning("InterruptedException occurred in waitForResponse.");
}
}
}
/**
* スコープを分割して、配列に変換します.
* @param scope スコープ
* @return 分割されたスコープ. scopeが<code>null</code>または空文字の場合は<code>null</code>
*/
private String[] parseScopes(final String scope) {
if (scope == null) {
return null;
}
String[] scopes = scope.split(",");
for (int i = 0; i < scopes.length; i++) {
String s = scopes[i].trim();
if (s.equals("")) {
return null;
}
scopes[i] = s;
}
return scopes;
}
}