geektimes

Подключаем FB, VK, G+ в Android. Версия Light

  • вторник, 18 ноября 2014 г. в 02:10:52
http://habrahabr.ru/post/243411/

Встала передо мной задача – сделать постинг ссылок из Андроида в пару-тройку соцсетей. Причем, максимально простой и легкий – чтобы не плодить сущности и как можно меньше заморачиваться с токенами, сессиями и прочая. Задача, действительно, минимум – только размещение ссылки в собственном аккаунте пользователя. Если к ссылке можно легко добавить описания или картинки – сделать, но не упираться.

В силу разных причин были выбраны Facebook, Vkontakte и Google+. Планировала добавить Twitter, но его Fabric к тому времени еще не вышел, а использовать стороннюю библиотеку не хотелось (см. п.2 ниже). Позже добавлю.

В итоге, задача для этих трех соцсетей получилась следующей:

  1. Максимально простой программный интерфейс постинга ссылок.
  2. Использование только нативных SDK (из тех соображений, что эти знания пригодятся в дальнейшем).
  3. Минимум кода – только самое необходимое для работы.
  4. Работать все должно вне зависимости от наличия у пользователя установленного клиента соцсети. Но если он есть – использовать диалоговые окна клиента.
  5. Пользователю должно выводиться сообщение об успешном или нет размещении записи.
  6. Должна быть возможность программно реагировать на успешное размещение записи.


Архитектура решения


Вариант с отдельной библиотекой в конечном итоге оказался чересчур громоздким – итоговый код составил порядка 250 строк вместе с комментариями и пустыми строками. Причем, все инкапсулировалось в один класс, использовать который достаточно просто.

Поскольку ряд объектов из SDK все же пришлось встраивать в жизненный цикл, то нужен был некий контейнер – или Activity, или Fragment. Я выбрала решение с abstract Activity, т.к. мне нужно было вешать команды постинга на кнопку ActionBar. Причем, делать это для нескольких Activity с разными фрагментами внутри.

В abstract Activity я зашила весь код управления постингом, а наружу выставила 3 метода – по одному для каждой сетки. От этой самой abstract Activity потом унаследовала остальные Activity приложения, в которых были нужны соцсети. В них доступ к нужному коду свелся к вызову метода из родительского класса.

Все, что здесь описано, наверняка можно сделать и с фрагментом. Хотя где-то я наталкивалась на сообщение, что встраивание в жизненный цикл все равно нужно делать для родительской Activity. Не знаю, не пробовала.

Итак…

Чтобы описанный ниже код заработал, нужно подключить в проект для каждой соцсети свой SDK и зарегистрировать приложение в разделе разработчиков. Все описано здесь:

При разработке использовались описания SDK на родных сайтах соцсетей, статьи на Хабре и SO, код библиотеки ASNE (спасибо автору!)

Google+


Самым простым кодом логично оказалось решение для G+. Вся работа с аутентификацией и сессиями уже встроена в систему. Нужное диалоговое окно (нативное от клиента или веб, если клиента нет) выбирается само, и само же сообщает об успешном постинге. Вот в последнем и крылась единственная засада – реализация п.6 требований. Для отслеживания успешности публикации пришлось добавить одну константу и один условный оператор в onActivityResult:

	private static final int GOOGLEPLUS_REQUEST_CODE = 1001;

	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
            ...
	    if ((requestCode == GOOGLEPLUS_REQUEST_CODE) && (resultCode == -1)) {
	    	//Do something if success
	    }
	} 

Сама процедура постинга:

	/**
	* Publish link in Google+
	* @param text - message about link (may be changed or deleted by user)
	* @param link - http:// etc
	*/
	public final void googleplusPublish(String text, String link) {
		Intent shareIntent = new PlusShare.Builder(this)
                       .setType("text/plain")
                       .setText(text)
                       .setContentUrl(Uri.parse(link))
                       .getIntent();
		startActivityForResult(shareIntent, GOOGLEPLUS_REQUEST_CODE);
	}

FaceBook


C Фейсбуком пришлось повозиться подольше, и не все требования удалось реализовать.

Получилось:
  • обойтись без токенов, сессий и пр.;
  • добавить к ссылке кучу описаний и картинку – в G+ и VK такого функционала нет или он реализуется нетривиально;
  • использовать нативного клиента, если он установлен, но проверку и вызов нужного диалога в отличие от G+ и VK пришлось писать ручками.

Не получилось:
  • отследить закрытие пользователем диалогового окна (Cancel), если клиент установлен (для веб-диалога все работает) – в этом случае слушатель (см. код ниже) радостно рапортовал об успешной публикации записи. Поиски решения увенчались сомнительным успехом – если не реализована аутентификация из самого приложения, то в принципе невозможно отследить закрытие окна без публикации записи. Невозможно от слова «совсем». Городить только ради этого обработку авторизации и сессий не хотелось, поэтому пришлось выкручиваться текстом сообщения пользователю.

Что в коде…

Прежде всего, надо добавить в string.xml айдишник приложения и вписать в манифест метаданные (не забыв разрешить доступ в интернет – это для всех сетей надо!):

        <meta-data
            android:name="com.facebook.sdk.ApplicationId"
            android:value="@string/facebook_app_id"/>

Для работы понадобится вспомогательный объект, который довольно плотно «садится» на методы жизненного цикла. Он нужен для постинга нативным клиентом, если тот установлен. Можно было бы, конечно, обойтись только вебом, но без сессий тот требует ввода пароля каждый раз, что сильно раздражает. А раздражать пользователя лишний раз нехорошо…

Отслеживание успешности публикации тоже пришлось писать ручками. Вот здесь и вылезла та самая бяка с клиентом:

	private UiLifecycleHelper fbUIHelper;

	protected void onCreate(Bundle savedInstanceState) {
            ...
	    fbUIHelper = new UiLifecycleHelper(this, null);
	    fbUIHelper.onCreate(savedInstanceState);
        }

	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
	    ...
	    fbUIHelper.onActivityResult(requestCode, resultCode, data, new FacebookDialog.Callback() {
	    	//Listener for Facebook-client if installed
	    	@Override
	    	public void onError(FacebookDialog.PendingCall pendingCall, Exception error, Bundle data) {
	    		toastMessage("Запись не опубликована");
	    	}

	    	@Override
	    	public void onComplete(FacebookDialog.PendingCall pendingCall, Bundle data) {
	    		toastMessage("Если вы сами не отменили команду, то запись опубликована");
	    	}
	    });

	protected void onResume() {
	   ...
	    fbUIHelper.onResume();
	}

	protected void onSaveInstanceState(Bundle outState) {
	    ...
	    fbUIHelper.onSaveInstanceState(outState);
	}

	protected void onPause() {
	    ...
	    fbUIHelper.onPause();
	}

	protected void onDestroy() {
	    ...
	    fbUIHelper.onDestroy();
	}

Сам код метода, который будет использоваться дочерними классами, выглядит следующим образом:

	/**
	* Publish link in FaceBook
	* @param name - title of block
	* @param caption - text on bottom of block
	* @param description - description of link (between title and caption)
	* @param link - http:// etc
	* @param pictureLink - http:// etc - link on image in web
	*/
	public final void facebookPublish(String name, String caption, String description, String link, String pictureLink) {
  	  if (FacebookDialog.canPresentShareDialog(getApplicationContext(), FacebookDialog.ShareDialogFeature.SHARE_DIALOG)) {
  		  //Facebook-client is installed
  		  FacebookDialog shareDialog = new FacebookDialog.ShareDialogBuilder(this)
	    	.setName(name)
	    	.setCaption(caption)
	    	.setDescription(description)
	    	.setLink(link)
	    	.setPicture(pictureLink)
	    	.build();
  		  fbUIHelper.trackPendingDialogCall(shareDialog.present());	    	  
	  } else {
		  //Facebook-client is not installed – use web-dialog
		  Bundle params = new Bundle();
		  params.putString("name", name);
		  params.putString("caption", caption);
		  params.putString("description", description);
		  params.putString("link", link);
		  params.putString("picture", pictureLink);
		  WebDialog feedDialog = new WebDialog.FeedDialogBuilder(this, Utility.getMetadataApplicationId(this), params)
		  	.setOnCompleteListener(new OnCompleteListener() {
		  		//Listener for web-dialog
		  		@Override
		        public void onComplete(Bundle values, FacebookException error) {
		  			if ((values != null) && (values.getString("post_id") != null) && (error == null)) {
		  				toastMessage("Запись опубликована");
		  			} else {
		  				toastMessage("Запись не опубликована");
		  			};
		  		};
		  	})
		  	.build();
		  feedDialog.show();
	  }
	}

ВКонтакте


Пожалуй, самая жуткая документация. Пришлось изрядно попотеть. К тому же, полностью избавиться от использования токенов не удалось – без них никак. Но по порядку…

Здесь тоже придется залезть в манифест ради одной строки кода:

	<activity android:name="com.vk.sdk.VKOpenAuthActivity"/>

В доках мягко написано, что «стоит добавить, иначе могут быть проблемы с запуском авторизационной activity», но если у пользователя не стоит VK-клиент, то приложение вылетит с ошибкой ActivityNotFoundException.

В самом классе в самом начале надо добавить код для хранения айдишника, прав доступа и управления авторизацией:

	private String appId = "1234567"; // Need to change to real app_id
	private static String vkTokenKey = "VK_ACCESS_TOKEN";
	private static String[] vkScope = new String[]{VKScope.WALL};
	private final VKSdkListener vkSdkListener = new VKSdkListener() {
		@Override
	    public void onCaptchaError(VKError captchaError) {
	    	new VKCaptchaDialog(captchaError).show();
		}
	    @Override
	    public void onTokenExpired(VKAccessToken expiredToken) {
	    	VKSdk.authorize(vkScope, true, false);
	    }
	    @Override
	    public void onAccessDenied(VKError authorizationError) {
	    	new AlertDialog.Builder(SocialNetworkActivity.this)
	        	.setMessage(authorizationError.errorMessage)
	            .show();
	    }
	    @Override
	    public void onReceiveNewToken(VKAccessToken newToken) {
	    	newToken.saveTokenToSharedPreferences(getApplicationContext(), vkTokenKey);
	    }
	 };

В методы жизненного цикла Activity эта зараза тоже влезает глубоко, хотя и не так как ФБ:

	protected void onCreate(Bundle savedInstanceState) {
	    ...
            VKUIHelper.onCreate(this);
	    VKSdk.initialize(vkSdkListener, appId, VKAccessToken.tokenFromSharedPreferences(this, vkTokenKey));
	}

	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
	    ...
	    VKUIHelper.onActivityResult(this, requestCode, resultCode, data); 
	} 

	protected void onResume() {
	    ...
	    VKUIHelper.onResume(this); 
	}

	protected void onDestroy() {
	    ...
	    VKUIHelper.onDestroy(this); 
	}

Однако при публикации ссылки придется заставить пользователя сначала авторизоваться, а потом уже повторно жать на кнопку публикации. Это плохо с точки зрения юзабилити, но другого варианта я не нашла. Выданный один раз токен вроде как действует около часа. Геморно, но не хуже регистрации при каждом постинге, как в веб-диалоге ФБ.

Еще одна засада скрывалась там, где я подставы не ожидала ну никак. При отладке из Эклипса на реальном устройстве все работало как часы. Но стоило мне установить то же самое приложение из Гугл Плея, как VK-клиент при постинге ссылки начал ругаться:

{«error»:«invalid_request»,«error_description»:«sdk_fingerprint is incorrect»}

При этом авторизация проходит успешно, да и то же самое приложение без VK-клиента через веб работает прекрасно. Т.е. fingerprint корректный. Подозреваю глюк клиента. Или не глюк, а намеренно закрытая возможность, что вполне вероятно, глядя на вот эту ссылку (ближе к концу). Буду разбираться дальше, но если кто в курсе и может помочь – буду признательна.

Еще один нерешенный вопрос – почему заголовок ссылки, видимый в окне предпросмотра, теряется при публикации. Ответа не нашла, увы. Тоже буду признательна за подсказки.

Сам код постинга:

	/**
	* Publish link in Vkontakte
	* @param message - message about link (may be changed or deleted by user)
	* @param link - http:// etc
	* @param linkName - title of link - not published (don't know why...)
	*/
	public final void vkontaktePublish(String message, String link, String linkName) {
		VKAccessToken token = VKAccessToken.tokenFromSharedPreferences(this, vkTokenKey);
		if ((token == null) || token.isExpired()) {
			VKSdk.authorize(vkScope, true, false);
			toastMessage("Требуется авторизация. После нее повторите попытку публикации");
		} else {
			new VKShareDialog()
	        .setText(message)
	        .setAttachmentLink(linkName, link)
	        .setShareDialogListener(new VKShareDialog.VKShareDialogListener() {
	            @Override
	            public void onVkShareComplete(int postId) {
	            	toastMessage("Запись опубликована");
	            }
	            @Override
	            public void onVkShareCancel() {
	            	toastMessage("Запись не опубликована");
	            }
	        }).show(getSupportFragmentManager(), "VK_SHARE_DIALOG");    
		} 
	}

Я писала приложение под Support Library, поэтому в коде используется getSupportFragmentManager. Для версий 3.0+ надо заменить его на вызов родного метода.

Использование кода


Ну, вот, вкратце и все. Теперь наследуем нужную Activity от этой и где требуется используем вызовы хххPublish(). Можно на кнопки вешать, а можно и на popup-меню в ActionBar (правда, в этом случае вызов будет не очень красивый, зато само меню рабочее):

    public boolean onOptionsItemSelected(MenuItem item) {
		switch (item.getItemId()) {
			case R.id.action_share:
				View view = findViewById(R.id.action_share);
				showPopupMenu(view, 
						"https://play.google.com/store/apps/details?id=ru.fantaversum.taleidoscope",
						"Талейдоскоп - бесплатная библиотека рассказов",
						"Талейдоскоп на Google Play",
						"Рассказы и повести с авторскими байками - все бесплатно и без рекламы. Тексты отобраны издательствами. Регулярные обновления.",
						"http://www.taleidoscope.ru/images/fb_logo.png",
						"Рекомендую: Талейдоскоп - бесплатная библиотека. Рассказы и повести с авторскими байками - все бесплатно и без рекламы. Тексты отобраны издательствами. Регулярные обновления.",
						"Талейдоскоп на Google Play");
				break;
		}
        return super.onOptionsItemSelected(item);
    }

    public void showPopupMenu(View view, final String link, 
    							final String fb_name, final String fb_caption, 
    							final String fb_description, final String fb_pictureLink,
    							final String message, final String linkName) {
        PopupMenu popup = new PopupMenu(this, view);
        popup.getMenuInflater().inflate(R.menu.popup, popup.getMenu());
        popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
            @Override
            public boolean onMenuItemClick(MenuItem menuItem) {
                switch (menuItem.getItemId()) {
                    case R.id.menu_facebook:
                        facebookPublish(fb_name, fb_caption, fb_description, link, fb_pictureLink);
                        return true;
                    case R.id.menu_vkontakte:
                        vkontaktePublish(message, link, linkName);
                        return true;
                    case R.id.menu_googleplus:
                        googleplusPublish(message, link);
                        return true;
                }
                return false;
            }
        });
        popup.show();
    }

Исходный код всей Activity можно скачать здесь.