2013년 3월 28일 목요일

[Android][Google Cloud Storage] Upload A File to Google Cloud Storage in Android

Android 폰에서 생성이 되는 data file들을 자동으로 공유 받기 위해서 cloud server를 이용할 수 있다. 대표적인 storage server로는 Amazon S3, Google Cloud Storage, MS Azure등이 있다. 여기선 Google Cloud Storage(GS)를 사용해서, 휴대폰의 데이터를 server로 upload 한는 방법을 정리해 보겠다.
GS 사용 관련 기본적인 사항은 여기에서 확인 할 수 있다. Google 의 많은 서비스 들과는 달리, GS는 초기 5GB 바이트 까지 무료사용 할 수 있지만, 서비스를 activate하기 위해선 billing 정보를 먼저 등록해야 한다. Google APIs Console 사이트의 Services 탭에서 Google Cloud Storage를 on 시키고 Billing 정보를 입력한 다음 API Access 탭에서 Service account 하나 만들자. Google service들을 사용하기 위해선 사용을 원하는 client(여기선 android application)이 일련의 인증과정을 거쳐서 authentication을 먼저 획득해야 한다. 다양한 종류의 인증 방법이 존재하나, 여기서는 사용자의 interaction 없이, 우리가 만들 application이 스스로 access 권한을 획득해서 GS를 사용해야 하므로 service account를 생성하도록 한다.

Service Account를 생성하면, 우리는 private key(.p12) 파일과(개인 PC에 저장) service account에 대한 EMAIL address를 발급받게 된다. 이 두가지를 가지고 GS service에 접근할 수 있는 권한을 획득할 수 있다.
String SERVICE_ACCOUNT_EMAIL = "4XXXe@developer.gserviceaccount.com";
String BUCKET_NAME = "my_bucket_name";
String STORAGE_SCOPE = "https://www.googleapis.com/auth/devstorage.full_control";
HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
JsonFactory JSON_FACTORY = new JacksonFactory();
String KeyFile = "privatekey.p12";

File privateKey = new File(keyFile);
GoogleCredential credential = new GoogleCredential.Builder()
 .setTransport(HTTP_TRANSPORT)
 .setJsonFactory(JSON_FACTORY)
 .setServiceAccountId(SERVICE_ACCOUNT_EMAIL)
 .setServiceAccountScopes(STORAGE_SCOPE)
 .setServiceAccountPrivateKeyFromP12File(privateKey)
 .build();

여기에서 중요한 것은 SERVICE_ACCOUNT_EMAIL에는 service account 생성에서 발급된 EMAIL address를 넣어주고, KeyFile 역시 service account시에 발급된 key file을 지정해 준다.

GS는 데이터가 저장되는 공간을 bucket단위로 관리한다. Bucket에 저장된 file 리스트에 대한 정보를 얻기위해선 HTTP GET method를 이용한다.
String URI = "https://storage.googleapis.com/" + BUCKET_NAME;
GenericUrl url = new GenericUrl(URI);
try {
 HttpRequestFactory requestFactory = HTTP_TRANSPORT.createRequestFactory(credential);
 HttpRequest request = requestFactory.buildGetRequest(url);
 HttpResponse response = request.execute();
 System.out.println(response.parseAsString());
} catch (IOException e) {
 e.printStackTrace();
}

위의 예제에서 url에 file name을 추가하게 되면, 지정된 file을 download할 수 있다.
String fileName = "downloadFile.file"
String URI = "https://storage.googleapis.com/" + BUCKET_NAME + "/";
GenericUrl url = new GenericUrl(URI);
url.appendRawPath(fileName);
try {
 HttpRequestFactory requestFactory = HTTP_TRANSPORT.createRequestFactory(credential);
 HttpRequest request = requestFactory.buildGetRequest(url);
 HttpResponse response = request.execute();
 File mediaFile = new File(fileName);
 OutputStream out = new FileOutputStream(mediaFile);
 response.download(out);
} catch (IOException e) {
 e.printStackTrace();
}

File delete code는 HTTP DELETE method를 이용한다.
String fileName = "deleteFile.file"
String URI = "https://storage.googleapis.com/" + BUCKET_NAME + "/";
GenericUrl url = new GenericUrl(URI);
url.appendRawPath(fileName);
try {
 HttpRequestFactory requestFactory = HTTP_TRANSPORT.createRequestFactory(credential);
 HttpRequest request = requestFactory.buildDeleteRequest(url);
 HttpResponse response = request.execute();
 System.out.println(response.parseAsString());
} catch (IOException e) {
 e.printStackTrace();
}

File upload를 위해선 HTTP PUT method를 이용한다.
String fileName = "uploadFile.file"
String URI = "https://storage.googleapis.com/" + BUCKET_NAME + "/";
GenericUrl url = new GenericUrl(URI);
url.appendRawPath(fileName);
try {         
 File mediaFile = new File(fileName);
 InputStreamContent mediaContent =
 new InputStreamContent("text/plain", //"image/jpeg"
  new BufferedInputStream(new FileInputStream(mediaFile)));
 mediaContent.setLength(mediaFile.length());

 HttpRequestFactory requestFactory = HTTP_TRANSPORT.createRequestFactory(credential);
 HttpRequest postrequest = requestFactory.buildPutRequest(url, mediaContent);
 HttpResponse response = postrequest.execute();
} catch (IOException e) {
 System.err.println(e.getMessage());
}

위의 코드는 google site에서 제공된 sample을 추가 보완하여 작성하였다

2013년 2월 14일 목요일

[Android][GAE] Upload a file to GAE server in Android Using HTTP POST

Google App Engine에서는 다양한 형태의 data object(text, image, sound, etc.)를 저장하기 위해 Blobstore 라는 service를 제공한다. 저장되는 파일의 max size는 32MB 이다.

상세내용은 아래의 링크를 참고하면 된다.
Google App Engine Blob Page (Python)

여기에서 제공되는 sample code의 일부를 아래에 발췌했다.
class MainHandler(webapp2.RequestHandler):
 def get(self):
  upload_url = blobstore.create_upload_url('/upload')
  self.response.out.write('<html><body>')
  self.response.out.write('<form action="%s" method="POST" 
       enctype="multipart/form-data">' % upload_url)
  self.response.out.write("""Upload File: <input type="file" name="file"> <br>
       <input type="submit" name="submit" value="Submit"> </form></body></html>""")

class UploadHandler(blobstore_handlers.BlobstoreUploadHandler):
 def post(self):
  upload_files = self.get_uploads('file')  # 'file' is file upload field in the form
  blob_info = upload_files[0]
  self.redirect('/serve/%s' % blob_info.key())
HTTP POST request를 통해서 선택한 file을 blob store에 저장하는 코드이다. 여기서 주의깊게 봐야 할 부분은 데이터가 저장되는 공간에 대한 url을 upload_url = blobstore.create_upload_url('/upload')를 통해 dynamic 하게 생성한다는 것이다. 한번 생성된 url은 10분 동안 유효하다. 즉, 10분 동안은 이 url을 blob store에 file을 저장하기위해 어디서든 사용할 수 있다는 뜻이다. upload_files = self.get_uploads('file')에서는 파일을 실제로 저장한다. blob_info.key()는 저장된 방금 저장된 file을 access 하기 위한 정보를 담고 있다.

위의 코드가 실행된 web page에서 소스보기를 하면 upload_url로 어떤 값이 설정됐는지 확인 할 수 있다.
다음과 같은 형태를 볼 수 있는데, action="http://xxx.appspot.com/_ah/upload/AMmfu6ZqsMW_ ... cs32MZ/" 여기서 " " 안의 값이 upload_url이다.

이 값을 사용해서 android에서 text file을 upload해 보는 코드를 아래와 같이 작성해 보았다.
static void fileup() {
 HttpClient httpclient = new DefaultHttpClient();
 HttpPost httppost = new HttpPost(upload_url); 
 try {
  MultipartEntity entity = new MultipartEntity();
  entity.addPart("file", new FileBody(new File(fileNameToSend, "text/plain"));
  httppost.setEntity(entity);
  HttpResponse response = httpclient.execute(httppost);
 } catch (ClientProtocolException e) {
 } catch (IOException e) {
 }
}
코드의 일부는 http://hc.apache.org/ 에서 제공하는 http library를 사용하였다.

upload된 파일은 Google App Engine의 Blob Viewer메뉴에서 확인할 수 있다.
Android에서 GAE의 blobstore에 저장이 잘 되는것은 확인 했지만, upload url을 android상에서 바로 생성할 수 없기 때문에 조금 다른 방식이 필요하다.
To be continued...

2013년 2월 13일 수요일

[Android] Automatic Running a Service after booting up

어떠한 background service가 사용자에 의해서 실행이 된 후에 폰이 재 부팅이 되는 일이 발생한다면, 사용자는 한번 실행 시켜둔 서비스가 부팅 후에도 자동으로 실행이 될 것으로 기대할 수 있다. 이런 경우, 폰의 부팅이 완료되고 난 시점에 사용자의 설정상태(폰 종료 전 서비스를 실행시켰었는지)를 확인하여 만약 그 상태가 on 이었다면 자동으로 서비스를 실행 시켜줘야 한다.

이를 위해선 크게 두가지를 해야한다.
1. 폰이 부팅이 된 시점을 알고, 그때에 일련의 작업을 처리할 수 있어야 한다.
2. 사용자의 서비스 실행 여부를 알 수 있어야 한다.

첫째로, android 시스템은 폰 부팅이 완료되고 나면 android.intent.action.BOOT_COMPLETED intent를 broadcast 한다. 이 intent를 받을려면 manifest 파일에 receiver를 등록하고 해당 intent를 추가해야 한다. App 내에서 code로 broadcast receiver/intent를 등록할 수 없는 이유는 app이 실행되기 전에 해당 intent를 받을 수 있어야 하기 때문이다. (코드로 등록한다는 것은 사용자가 앱을 실행해서 해당 루틴이 수행이 되어야 한다는 것을 전재로 한다.)

BOOT_COMPLETED intent를 받는 receiver code는 아래와 같다. onReceiver 루틴에서 부팅이 끝난 후 필요한 일을 처리하면 된다.
public class BootStartupReceiver extends BroadcastReceiver {

 @Override
 public void onReceive(Context context, Intent intent) {
  if(intent.getAction().equals("android.intent.action.BOOT_COMPLETED")){
   // After booting up, this routine is called
   // If s service was on before turned off, let's start here again
   ...
  }
 }
}
BOOT_COMPLETED intent를 받기 위해 AndroidManifest.xml 파일에 아래와 같이 permission과 receiver를 등록한다.
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

...


    
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    


이제 부팅이 끝나고 난 시점에 무언가를 처리할 수 있는 준비가 되었다. 그럼 두번째로 폰 종료 전 사용자의 서비스 실행여부에 대한 상태를 알 수 있어야 한다. 폰 재부팅 후에도 값을 유지하기 위해선 정보가 non-volatile 메모리, 즉 flash에 저장이 되어야 한다. Android에서는 이를 위해 크게 세가지 정도의 방법을 제공한다.
1. Preference
2. File System
3. SQLite DB
하지만, 여기서 저장해야 할 값은 boolean type의 간략한 정보이므로 preference를 이용하는게 적절하겠다.

사용자가 서비스를 on 할때, 아래와 같이 shared preference에 상태값을 저장한다.
public static final String PREF_FILE_NAME = "PrefFile";

// Read saved value
SharedPreferences preferences = getSharedPreferences(PREF_FILE_NAME, MODE_PRIVATE);
boolean serviceOn = preferences.getBoolean("serviceOn", false);
if(serviceOn){
 // Do something
}
폰 부팅 후, 저장된 상태값을 다시 읽어올 때는 아래와 같은 루틴을 사용하면 되겠다.
// Save setting
SharedPreferences preferences = getSharedPreferences(PREF_FILE_NAME, MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.putBoolean("serviceOn", isChecked);
editor.commit();

요약하면, 사용자가 background service를 실행할 때 그 상태를 preference로 저장하고, 폰이 재 부팅후 부팅 완료가 되는 시점에 preference에 저장해 두었던 값을 다시 읽어와서 그 상태가 on이면 자동으로 background service를 실행한다.

추가로, 화면이 켜져 있는 동안 혹은 사용자가 lock 화면을 해제한 동안만 서비스가 동작하게 하고싶다면, 아래의 intent를 받아서 적절히 처리하면 되겠다.
1. Intent.ACTION_SCREEN_ON
2. Intent.ACTION_SCREEN_OFF
3. Intent.ACTION_USER_PRESENT : 사용자가 lock 화면 해제시 발생
SCREEN ON/OFF의 경우는 AndroidManifest.xml파일에 등록하는 방법은 없다. 코드상에서 등록을 해야지만 수신할 수 있다. 3번의 경우는 코드와 manifest에 등록하는 방법 두가지가 모두 존재하지만 1, 2, 3번 의 경우 모두 필요할 때 코드상에서 추가하도록 하는게 적절해 보인다.
(3번의 경우, manifest로 등록하기 위해선 android.intent.action.USER_PRESENT 사용해야 한다.)

2013년 1월 16일 수요일

[Android] Top Activity Monitoring using AlarmManager

Android에서는 현재 실행중인 top activity에 대한 정보를 얻기위해선 ActivityManager를 이용하여 아래와 같이 직접 해당 내용을 읽어와야 한다.
ActivityManager am=null;
am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
String currentPackage = am.getRunningTasks(1).get(0).topActivity.getPackageName();
물론, AndroidManifest.xml파일에 아래와 같은 권한을 미리 추가해 둬야 한다.
<uses-permission android:name="android.permission.GET_TASKS" />

Android에서는 top activity의 변화를 알려주는 broadcast intent가 없기때문에, top activity의 변화를 monitoring 하기위해선 주기적으로 top activity의 변화를 polling해 봐야 한다. 주기적으로 수행 되는 일련의 작업을 위해선 AlarmManager를 이용하는 것이 가장 적절해 보인다. (자바에서 사용되던 TimerTask를 이용한 방법은 동작의 신뢰성이 보장이 되지 않는 다고 한다.)

아래는 AlarmManager를 이용해서 주기적으로 top activity의 변화를 확인하는 코드이다.

AlarmManagerBroadcastReceiver class:
public class AlarmManagerBroadcastReceiver extends BroadcastReceiver {
 private static String mPreviousPackage;
 
 private PendingIntent getPI(Context context)
 {
  Intent intent = new Intent(context, AlarmManagerBroadcastReceiver.class);
  PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, 0);
  return pi;
 }
 
 public void start(Context context, long interval) {
  AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
  long triggerAtMillis = System.currentTimeMillis();
  am.setRepeating(AlarmManager.RTC, triggerAtMillis, interval, getPI(context));
 }
 
 public void stop(Context context) {
  AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
  am.cancel(getPI(context));
 }
 
 @Override
 public void onReceive(Context context, Intent intent) {
  // The routine is periodically called by AlarmManager
  // Write code for a periodic work, here !!!
  ActivityManager am=null;
  am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
  String currentPackage = am.getRunningTasks(1).get(0).topActivity.getPackageName();
  if(!currentPackage.equals(mPreviousPackage)) {
   System.out.println("Top Activity Changed : pakcagename = " + currentPackage);
   mPreviousPackage = currentPackage;
  }
 }
}
그리고 AndroidManifest.xml에 아래와 같이 receiver를 등록한다. 만약 등록하지 않으면 설정한 알람메시지를 수신하지 못한다.



Main routine:
Context mAppContext=null;
AlarmManagerBroadcastReceiver mTaskMonitor; 
final long INTERVAL_MILLIS = 2000; // 2 seconds
...
mAppContext = getApplicationContext();
mTaskMonitor = new AlarmManagerBroadcastReceiver();
Start periodic monitoring:
mTaskMonitor.start(mAppContext, INTERVAL_MILLIS);
Stop periodic monitoring:
mTaskMonitor.stop(mAppContext);

매 2초마다 top activity의 변화를 확인해 보는 것이 적절한지는 아직 의문이 드나, 너무 길게 설정하는 경우는 Activity의 변화를 놓치는 경우도 발생할 수 있을 것 같다. 그리고, 해당 루틴은 LCD가 ON이 되고 난 후, ACTION_USER_PRESENT(사용자가 lock 화면을 해제한 후) 조건 하에서만 실행이 되도록 해야 한다. LCD OFF 상에서는 사용자의 개입이 없는 관계로 top activity의 변화는 존재하지 않기 때문이다. 이런 관점에서 보면 잦은 polling 일지라도, CPU를 직접 깨우는 일이 발생하지는 않게 되므로 battery의 소모에는 특별한 영향은 주지 않을 것으로 보인다.