Android 빠른 시작

이 가이드에서는 YouTube Data API에 요청하는 간단한 Android 애플리케이션을 설정하는 방법을 설명합니다.

기본 요건

이 빠른 시작을 실행하려면 다음이 필요합니다.

이 빠른 시작에서는 독립형 SDK 도구가 아닌 Android 스튜디오 IDE를 사용하고 스튜디오 프로젝트 내에서 파일을 쉽게 찾고 만들고 수정하는 데 익숙하다고 가정합니다.

1단계: SHA1 지문 획득

터미널에서 다음 Keytool 유틸리티 명령어를 실행하여 API를 사용 설정하는 데 사용할 SHA1 지문을 가져옵니다.

keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore -list -v

키 저장소 비밀번호를 묻는 메시지가 표시되면 'android'를 입력합니다.

Keytool은 지문을 셸에 출력합니다. 예를 들면 다음과 같습니다.

$ keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore -list -v
Enter keystore password: Type "android" if using debug.keystore
Alias name: androiddebugkey
Creation date: Dec 4, 2014
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: CN=Android Debug, O=Android, C=US
Issuer: CN=Android Debug, O=Android, C=US
Serial number: 503bd581
Valid from: Mon Aug 27 13:16:01 PDT 2012 until: Wed Aug 20 13:16:01 PDT 2042
Certificate fingerprints:
   MD5:  1B:2B:2D:37:E1:CE:06:8B:A0:F0:73:05:3C:A3:63:DD
   SHA1: D8:AA:43:97:59:EE:C5:95:26:6A:07:EE:1C:37:8E:F4:F0:C8:05:C8
   SHA256: F3:6F:98:51:9A:DF:C3:15:4E:48:4B:0F:91:E3:3C:6A:A0:97:DC:0A:3F:B2:D2:E1:FE:23:57:F5:EB:AC:13:30
   Signature algorithm name: SHA1withRSA
   Version: 3

위의 예에서 강조표시된 SHA1 지문을 복사합니다.

2단계: YouTube Data API 사용 설정

  1. 이 마법사를 사용하여 Google Developers Console에서 프로젝트를 만들거나 선택하고 API를 자동으로 사용 설정합니다. 계속을 클릭한 다음 사용자 인증 정보로 이동을 클릭합니다.

  2. 사용자 인증 정보 만들기 페이지에서 취소 버튼을 클릭합니다.

  3. 페이지 상단에서 OAuth 동의 화면 탭을 선택합니다. 이메일 주소를 선택하고, 아직 설정되지 않은 경우 제품 이름을 입력한 후 저장 버튼을 클릭합니다.

  4. 사용자 인증 정보 탭을 선택하고 사용자 인증 정보 만들기 버튼을 클릭한 후 OAuth 클라이언트 ID를 선택합니다.

  5. 애플리케이션 유형으로 Android를 선택합니다.
  6. 1단계의 SHA1 지문을 서명 인증서 지문 필드에 복사합니다.
  7. 패키지 이름 필드에 com.example.quickstart를 입력합니다.
  8. 만들기 버튼을 클릭합니다.

3단계: 새 Android 프로젝트 만들기

  1. Android 스튜디오를 열고 새 Android 스튜디오 프로젝트를 시작합니다.
  2. 새 프로젝트 화면에서 애플리케이션 이름을 '빠른 시작'으로 지정합니다.
  3. Company Domain을 'example.com'으로 설정하고 자동으로 생성된 패키지 이름이 2단계에서 Developer Console에 입력한 이름과 일치하는지 확인합니다. Next를 클릭합니다.
  4. Target Android Devices 화면에서 Phone and Tablet 체크박스를 선택하고 Minimum SDK에서 'API 14: Android 4.0(IceCreamSandwich)'을 선택합니다. 다른 체크박스는 선택하지 않은 상태로 둡니다. Next를 클릭합니다.
  5. Add an activity to Mobile 화면에서 Add No Activity를 클릭합니다.
  6. Finish를 클릭합니다.

이 시점에서 Android 스튜디오가 프로젝트를 만들고 엽니다.

4단계: 프로젝트 준비

프로젝트 사이드바는 Android 스튜디오에서 생성한 기본 프로젝트 파일의 확장 가능한 목록입니다. 이 목록에서 Gradle 스크립트 목록을 펼치고 프로젝트가 아닌 '앱' 모듈과 연결된 build.gradle 파일을 엽니다.

  1. build.gradle 파일을 열고 콘텐츠를 다음으로 바꿉니다.
  2. 툴바에서 Tools > Android > Sync Project with Gradle Files를 선택합니다. 이렇게 하면 프로젝트에 필요한 라이브러리를 가져와 사용할 수 있게 됩니다.
  3. 기본 src/main/AndroidManifest.xml 파일을 찾아 엽니다. 프로젝트 사이드바에서 이 파일은 app 아래, 그리고 manifests 아래에 중첩되어 있습니다. 파일 콘텐츠를 다음 코드로 바꿉니다.
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.quickstart">
    
        <uses-permission android:name="android.permission.INTERNET" />
        <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
        <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    
        <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="YouTube Data API Android Quickstart"
            android:theme="@style/AppTheme" >
            <activity
                android:name=".MainActivity"
                android:label="YouTube Data API Android Quickstart" >
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
    
        </application>
    </manifest>

5단계: 샘플 설정

새 Java 클래스를 만듭니다. 이렇게 하려면 먼저 프로젝트 사이드바에서 java 폴더를 선택합니다. 이 폴더는 app개 파일 그룹에 표시됩니다. 폴더를 클릭한 후 메뉴 바에서 File > New > Java Class를 선택하거나 폴더를 마우스 오른쪽 버튼으로 클릭하고 New > Java Class를 선택합니다. 디렉터리를 선택하라는 메시지가 표시되면 .../app/src/main/java를 선택합니다.

클래스 이름을 'MainActivity'로 지정하고 OK를 클릭합니다. 새 파일의 내용을 다음 코드로 바꿉니다.

package com.example.quickstart;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.api.client.extensions.android.http.AndroidHttp;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.googleapis.extensions.android.gms.auth.GooglePlayServicesAvailabilityIOException;
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException;

import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.ExponentialBackOff;

import com.google.api.services.youtube.YouTubeScopes;

import com.google.api.services.youtube.model.*;

import android.Manifest;
import android.accounts.AccountManager;
import android.app.Activity;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.text.method.ScrollingMovementMethod;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import pub.devrel.easypermissions.AfterPermissionGranted;
import pub.devrel.easypermissions.EasyPermissions;

public class MainActivity extends Activity
    implements EasyPermissions.PermissionCallbacks {
    GoogleAccountCredential mCredential;
    private TextView mOutputText;
    private Button mCallApiButton;
    ProgressDialog mProgress;

    static final int REQUEST_ACCOUNT_PICKER = 1000;
    static final int REQUEST_AUTHORIZATION = 1001;
    static final int REQUEST_GOOGLE_PLAY_SERVICES = 1002;
    static final int REQUEST_PERMISSION_GET_ACCOUNTS = 1003;

    private static final String BUTTON_TEXT = "Call YouTube Data API";
    private static final String PREF_ACCOUNT_NAME = "accountName";
    private static final String[] SCOPES = { YouTubeScopes.YOUTUBE_READONLY };

    /**
     * Create the main activity.
     * @param savedInstanceState previously saved instance data.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        LinearLayout activityLayout = new LinearLayout(this);
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.MATCH_PARENT);
        activityLayout.setLayoutParams(lp);
        activityLayout.setOrientation(LinearLayout.VERTICAL);
        activityLayout.setPadding(16, 16, 16, 16);

        ViewGroup.LayoutParams tlp = new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);

        mCallApiButton = new Button(this);
        mCallApiButton.setText(BUTTON_TEXT);
        mCallApiButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mCallApiButton.setEnabled(false);
                mOutputText.setText("");
                getResultsFromApi();
                mCallApiButton.setEnabled(true);
            }
        });
        activityLayout.addView(mCallApiButton);

        mOutputText = new TextView(this);
        mOutputText.setLayoutParams(tlp);
        mOutputText.setPadding(16, 16, 16, 16);
        mOutputText.setVerticalScrollBarEnabled(true);
        mOutputText.setMovementMethod(new ScrollingMovementMethod());
        mOutputText.setText(
                "Click the \'" + BUTTON_TEXT +"\' button to test the API.");
        activityLayout.addView(mOutputText);

        mProgress = new ProgressDialog(this);
        mProgress.setMessage("Calling YouTube Data API ...");

        setContentView(activityLayout);

        // Initialize credentials and service object.
        mCredential = GoogleAccountCredential.usingOAuth2(
                getApplicationContext(), Arrays.asList(SCOPES))
                .setBackOff(new ExponentialBackOff());
    }

    

    /**
     * Attempt to call the API, after verifying that all the preconditions are
     * satisfied. The preconditions are: Google Play Services installed, an
     * account was selected and the device currently has online access. If any
     * of the preconditions are not satisfied, the app will prompt the user as
     * appropriate.
     */
    private void getResultsFromApi() {
        if (! isGooglePlayServicesAvailable()) {
            acquireGooglePlayServices();
        } else if (mCredential.getSelectedAccountName() == null) {
            chooseAccount();
        } else if (! isDeviceOnline()) {
            mOutputText.setText("No network connection available.");
        } else {
            new MakeRequestTask(mCredential).execute();
        }
    }

    /**
     * Attempts to set the account used with the API credentials. If an account
     * name was previously saved it will use that one; otherwise an account
     * picker dialog will be shown to the user. Note that the setting the
     * account to use with the credentials object requires the app to have the
     * GET_ACCOUNTS permission, which is requested here if it is not already
     * present. The AfterPermissionGranted annotation indicates that this
     * function will be rerun automatically whenever the GET_ACCOUNTS permission
     * is granted.
     */
    @AfterPermissionGranted(REQUEST_PERMISSION_GET_ACCOUNTS)
    private void chooseAccount() {
        if (EasyPermissions.hasPermissions(
                this, Manifest.permission.GET_ACCOUNTS)) {
            String accountName = getPreferences(Context.MODE_PRIVATE)
                    .getString(PREF_ACCOUNT_NAME, null);
            if (accountName != null) {
                mCredential.setSelectedAccountName(accountName);
                getResultsFromApi();
            } else {
                // Start a dialog from which the user can choose an account
                startActivityForResult(
                        mCredential.newChooseAccountIntent(),
                        REQUEST_ACCOUNT_PICKER);
            }
        } else {
            // Request the GET_ACCOUNTS permission via a user dialog
            EasyPermissions.requestPermissions(
                    this,
                    "This app needs to access your Google account (via Contacts).",
                    REQUEST_PERMISSION_GET_ACCOUNTS,
                    Manifest.permission.GET_ACCOUNTS);
        }
    }

    /**
     * Called when an activity launched here (specifically, AccountPicker
     * and authorization) exits, giving you the requestCode you started it with,
     * the resultCode it returned, and any additional data from it.
     * @param requestCode code indicating which activity result is incoming.
     * @param resultCode code indicating the result of the incoming
     *     activity result.
     * @param data Intent (containing result data) returned by incoming
     *     activity result.
     */
    @Override
    protected void onActivityResult(
            int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        switch(requestCode) {
            case REQUEST_GOOGLE_PLAY_SERVICES:
                if (resultCode != RESULT_OK) {
                    mOutputText.setText(
                            "This app requires Google Play Services. Please install " +
                            "Google Play Services on your device and relaunch this app.");
                } else {
                    getResultsFromApi();
                }
                break;
            case REQUEST_ACCOUNT_PICKER:
                if (resultCode == RESULT_OK && data != null &&
                        data.getExtras() != null) {
                    String accountName =
                            data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
                    if (accountName != null) {
                        SharedPreferences settings =
                                getPreferences(Context.MODE_PRIVATE);
                        SharedPreferences.Editor editor = settings.edit();
                        editor.putString(PREF_ACCOUNT_NAME, accountName);
                        editor.apply();
                        mCredential.setSelectedAccountName(accountName);
                        getResultsFromApi();
                    }
                }
                break;
            case REQUEST_AUTHORIZATION:
                if (resultCode == RESULT_OK) {
                    getResultsFromApi();
                }
                break;
        }
    }

    /**
     * Respond to requests for permissions at runtime for API 23 and above.
     * @param requestCode The request code passed in
     *     requestPermissions(android.app.Activity, String, int, String[])
     * @param permissions The requested permissions. Never null.
     * @param grantResults The grant results for the corresponding permissions
     *     which is either PERMISSION_GRANTED or PERMISSION_DENIED. Never null.
     */
    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        EasyPermissions.onRequestPermissionsResult(
                requestCode, permissions, grantResults, this);
    }

    /**
     * Callback for when a permission is granted using the EasyPermissions
     * library.
     * @param requestCode The request code associated with the requested
     *         permission
     * @param list The requested permission list. Never null.
     */
    @Override
    public void onPermissionsGranted(int requestCode, List<String> list) {
        // Do nothing.
    }

    /**
     * Callback for when a permission is denied using the EasyPermissions
     * library.
     * @param requestCode The request code associated with the requested
     *         permission
     * @param list The requested permission list. Never null.
     */
    @Override
    public void onPermissionsDenied(int requestCode, List<String> list) {
        // Do nothing.
    }

    /**
     * Checks whether the device currently has a network connection.
     * @return true if the device has a network connection, false otherwise.
     */
    private boolean isDeviceOnline() {
        ConnectivityManager connMgr =
                (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
        return (networkInfo != null && networkInfo.isConnected());
    }

    /**
     * Check that Google Play services APK is installed and up to date.
     * @return true if Google Play Services is available and up to
     *     date on this device; false otherwise.
     */
    private boolean isGooglePlayServicesAvailable() {
        GoogleApiAvailability apiAvailability =
                GoogleApiAvailability.getInstance();
        final int connectionStatusCode =
                apiAvailability.isGooglePlayServicesAvailable(this);
        return connectionStatusCode == ConnectionResult.SUCCESS;
    }

    /**
     * Attempt to resolve a missing, out-of-date, invalid or disabled Google
     * Play Services installation via a user dialog, if possible.
     */
    private void acquireGooglePlayServices() {
        GoogleApiAvailability apiAvailability =
                GoogleApiAvailability.getInstance();
        final int connectionStatusCode =
                apiAvailability.isGooglePlayServicesAvailable(this);
        if (apiAvailability.isUserResolvableError(connectionStatusCode)) {
            showGooglePlayServicesAvailabilityErrorDialog(connectionStatusCode);
        }
    }


    /**
     * Display an error dialog showing that Google Play Services is missing
     * or out of date.
     * @param connectionStatusCode code describing the presence (or lack of)
     *     Google Play Services on this device.
     */
    void showGooglePlayServicesAvailabilityErrorDialog(
            final int connectionStatusCode) {
        GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance();
        Dialog dialog = apiAvailability.getErrorDialog(
                MainActivity.this,
                connectionStatusCode,
                REQUEST_GOOGLE_PLAY_SERVICES);
        dialog.show();
    }

    /**
     * An asynchronous task that handles the YouTube Data API call.
     * Placing the API calls in their own task ensures the UI stays responsive.
     */
    private class MakeRequestTask extends AsyncTask<Void, Void, List<String>> {
        private com.google.api.services.youtube.YouTube mService = null;
        private Exception mLastError = null;

        MakeRequestTask(GoogleAccountCredential credential) {
            HttpTransport transport = AndroidHttp.newCompatibleTransport();
            JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
            mService = new com.google.api.services.youtube.YouTube.Builder(
                    transport, jsonFactory, credential)
                    .setApplicationName("YouTube Data API Android Quickstart")
                    .build();
        }

        /**
         * Background task to call YouTube Data API.
         * @param params no parameters needed for this task.
         */
        @Override
        protected List<String> doInBackground(Void... params) {
            try {
                return getDataFromApi();
            } catch (Exception e) {
                mLastError = e;
                cancel(true);
                return null;
            }
        }

        /**
         * Fetch information about the "GoogleDevelopers" YouTube channel.
         * @return List of Strings containing information about the channel.
         * @throws IOException
         */
        private List<String> getDataFromApi() throws IOException {
            // Get a list of up to 10 files.
            List<String> channelInfo = new ArrayList<String>();
            ChannelListResponse result = mService.channels().list("snippet,contentDetails,statistics")
                 .setForUsername("GoogleDevelopers")
                 .execute();
            List<Channel> channels = result.getItems();
            if (channels != null) {
                Channel channel = channels.get(0);
                channelInfo.add("This channel's ID is " + channel.getId() + ". " +
                        "Its title is '" + channel.getSnippet().getTitle() + ", " +
                        "and it has " + channel.getStatistics().getViewCount() + " views.");
            }
            return channelInfo;
        }

        @Override
        protected void onPreExecute() {
            mOutputText.setText("");
            mProgress.show();
        }

        @Override
        protected void onPostExecute(List<String> output) {
            mProgress.hide();
            if (output == null || output.size() == 0) {
                mOutputText.setText("No results returned.");
            } else {
                output.add(0, "Data retrieved using the YouTube Data API:");
                mOutputText.setText(TextUtils.join("\n", output));
            }
        }

        @Override
        protected void onCancelled() {
            mProgress.hide();
            if (mLastError != null) {
                if (mLastError instanceof GooglePlayServicesAvailabilityIOException) {
                    showGooglePlayServicesAvailabilityErrorDialog(
                            ((GooglePlayServicesAvailabilityIOException) mLastError)
                                    .getConnectionStatusCode());
                } else if (mLastError instanceof UserRecoverableAuthIOException) {
                    startActivityForResult(
                            ((UserRecoverableAuthIOException) mLastError).getIntent(),
                            MainActivity.REQUEST_AUTHORIZATION);
                } else {
                    mOutputText.setText("The following error occurred:\n"
                            + mLastError.getMessage());
                }
            } else {
                mOutputText.setText("Request cancelled.");
            }
        }
    }
}

6단계: 앱 실행

  1. 앱을 테스트하려면 Run > Run app 메뉴 항목을 클릭합니다.
  2. 앱을 실행할 연결된 기기 (권장) 또는 에뮬레이션을 선택하라는 메시지가 표시됩니다. 에뮬레이션을 실행하는 경우 Google API 사용 시스템 이미지 중 하나를 사용하도록 구성되었는지 확인하세요. 현재 Google Play 서비스가 설치되지 않은 기기에서 빠른 시작을 실행하려고 하면 빠른 시작에서 이를 설치할 수 있는 대화상자가 생성됩니다.
  3. 에뮬레이터에서 실행 중인 경우 에뮬레이터가 완전히 시작되고 네트워크 연결이 설정될 수 있도록 허용합니다.
  4. 에뮬레이터를 처음 시작하는 경우 화면을 잠금 해제해야 할 수도 있습니다. 이와 관계없이 빠른 시작 앱은 자동으로 시작됩니다.
  5. 앱을 처음 실행하면 계정을 지정하라는 메시지가 표시됩니다. 로그인 과정을 완료하고 연결할 계정을 선택합니다.
  6. 계정을 선택하면 앱에 액세스를 승인하라는 메시지가 표시됩니다. 확인을 클릭하여 승인합니다.

Notes

  • 승인 정보는 앱과 함께 저장되므로 이후 실행 시 승인 요청 메시지가 표시되지 않습니다.

추가 자료

문제 해결

등록되지 않은 Android 애플리케이션

OAuth 대화상자에 '등록되지 않은 Android 애플리케이션'이라는 항목이 포함되어 있으면 2단계에서 만든 OAuth2 클라이언트 ID를 찾을 수 없으며 Android가 기본 클라이언트로 대체됩니다. 기본 클라이언트가 이 API를 사용하도록 구성되지 않으므로 accessNotConfigured.와 같은 오류와 함께 요청이 실패합니다. 이러한 오류에서는 기본 프로젝트 번호 608941808256를 언급할 수도 있습니다.

이 문제를 해결하려면 1단계에서 검색한 SHA1 디지털 지문과 build.gradle 파일에 나열된 applicationId가 Google Developers Console에서 설정한 값과 정확하게 일치해야 합니다.