Java tutorial
/* * Copyright (C) 2012 Tatsuo Nagamatsu <nagamatu@gmail.com> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.gmail.nagamatu.radiko.installer; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.message.BasicNameValuePair; import org.apache.http.protocol.HTTP; import android.accounts.Account; import android.accounts.AccountManager; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.view.LayoutInflater; import android.widget.EditText; import android.widget.ProgressBar; import android.widget.TextView; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; public class RadikoInstallerActivity extends Activity { private static final String URL_LOGIN = "https://www.google.com/accounts/ClientLogin"; private static final String URL_DOWNLOAD = "https://android.clients.google.com/market/api/ApiRequest"; private static final String URL_GOOGLE_TALK_PROVIDER = "content://com.google.android.gsf.gservices"; private static final String ACCOUNT_TYPE_HOSTED_OR_GOOGLE = "HOSTED_OR_GOOGLE"; private static final String LOGIN_SERVICE = "androidsecure"; private static final int PROTOCOL_VERSION = 2; private static final Pattern PATTERN_URL = Pattern.compile("https?:\\/\\/[^:]+"); private static final Pattern PATTERN_MARKETDA = Pattern.compile("MarketDA.*?(\\d+)"); private static final int DIALOG_SELECT_ACCOUNT = 0; private static final int DIALOG_PASSWD = 1; private static final int DIALOG_PROGRESS = 2; private static final int BUFSIZE = 4096; private static final String PARAMS_EMAIL = "Email"; private static final String PARAMS_PASSWD = "Passwd"; private static final String PARAMS_SERVICE = "service"; private static final String PARAMS_ACCOUNTTYPE = "accountType"; private static final String PACKAGE_NAME = "jp.radiko.Player"; private String mDeviceId; private Account[] mAccounts; private Account mAccount; private String mPasswd; private final Map<String, String> mLoginInfo = new HashMap<String, String>(); private static final Uri URI_GFS_SERVICE = Uri.parse(URL_GOOGLE_TALK_PROVIDER); private final HttpClient mClient = MySSLSocketFactory.getNewHttpClient(); private static String getDeviceId(Context context) { String id = null; Cursor c = context.getContentResolver().query(URI_GFS_SERVICE, null, null, new String[] { "android_id" }, null); try { c.moveToFirst(); id = Long.toHexString(Long.parseLong(c.getString(1))); } catch (Exception e) { e.printStackTrace(); } finally { if (c != null) { c.close(); } } return id; } /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mDeviceId = getDeviceId(this); if (mDeviceId == null) { updateMessage(R.string.error_download, "Device ID not found"); return; } updateMessage(R.string.check_account_passwd, null); getEmailAndPasswd(); } private void updateMessage(final int id, final String error) { runOnUiThread(new Runnable() { @Override public void run() { TextView view = (TextView) findViewById(R.id.message); view.setText(id); if (id == R.string.error_download) { final ProgressBar bar = (ProgressBar) findViewById(R.id.working); bar.setVisibility(ProgressBar.INVISIBLE); final TextView errormsg = (TextView) findViewById(R.id.error); errormsg.setText(error); } } }); } private void updateProgress(final int progress) { runOnUiThread(new Runnable() { @Override public void run() { final ProgressBar view = (ProgressBar) findViewById(R.id.progress); view.setProgress(progress); } }); } private void getEmailAndPasswd() { final AccountManager am = AccountManager.get(this); mAccounts = am.getAccountsByType("com.google"); mAccount = null; switch (mAccounts.length) { case 0: updateMessage(R.string.error_download, getResources().getString(R.string.no_google_account)); break; case 1: mAccount = mAccounts[0]; showDialog(DIALOG_PASSWD); break; default: showDialog(DIALOG_SELECT_ACCOUNT); break; } } private Dialog onCreateSelectAccountDialog() { final AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.select_account); final CharSequence[] items = new CharSequence[mAccounts.length]; int i = 0; for (Account a : mAccounts) { items[i++] = a.name; } builder.setItems(items, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { mAccount = mAccounts[which]; showDialog(DIALOG_PASSWD); } }); builder.setCancelable(false); return builder.create(); } private Dialog onCreatePasswdDialog() { final AlertDialog.Builder ab = new AlertDialog.Builder(this); ab.setPositiveButton(R.string.ok, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (dialog instanceof AlertDialog) { final AlertDialog dlg = (AlertDialog) dialog; final EditText passwd = (EditText) dlg.findViewById(R.id.dialog_text); if (passwd == null) { return; } mPasswd = passwd.getText().toString(); updateMessage(R.string.login_google, null); new Thread(new Runnable() { public void run() { login(); } }).start(); } } }); ab.setNegativeButton(R.string.cancel, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { finish(); } }); ab.setTitle("Password for " + mAccount.name); final LayoutInflater inflator = getLayoutInflater(); ab.setView(inflator.inflate(R.layout.passwd, null)); return ab.create(); } private Dialog onCreateProgressDialog() { final ProgressDialog dialog = new ProgressDialog(this); dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); dialog.setMessage("Loading..."); dialog.setCancelable(false); return dialog; } @Override public Dialog onCreateDialog(int id) { switch (id) { case DIALOG_SELECT_ACCOUNT: return onCreateSelectAccountDialog(); case DIALOG_PASSWD: return onCreatePasswdDialog(); case DIALOG_PROGRESS: return onCreateProgressDialog(); } return super.onCreateDialog(id); } private static String streamToString(InputStream resultStream) throws IOException { final BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(resultStream)); final StringBuffer res = new StringBuffer(); String aLine = reader.readLine(); while (aLine != null) { res.append(aLine + "\n"); aLine = reader.readLine(); } return res.toString(); } private List<NameValuePair> getParams() { final List<NameValuePair> params = new ArrayList<NameValuePair>(4); params.add(new BasicNameValuePair(PARAMS_EMAIL, mAccount.name)); params.add(new BasicNameValuePair(PARAMS_PASSWD, mPasswd)); params.add(new BasicNameValuePair(PARAMS_SERVICE, LOGIN_SERVICE)); params.add(new BasicNameValuePair(PARAMS_ACCOUNTTYPE, ACCOUNT_TYPE_HOSTED_OR_GOOGLE)); return params; } private void login() { try { final HttpPost request = new HttpPost(URL_LOGIN); request.addHeader("Content-type", "application/x-www-form-urlencoded"); request.setEntity(new UrlEncodedFormEntity(getParams(), HTTP.UTF_8)); final HttpResponse response = mClient.execute(request); final HttpEntity entity = response.getEntity(); if (response.getStatusLine().getStatusCode() >= 400) { updateMessage(R.string.error_download, response.getStatusLine().getReasonPhrase()); return; } final InputStream in = entity.getContent(); try { final DataInputStream din = new DataInputStream(in); String line; while ((line = din.readLine()) != null) { mLoginInfo.clear(); final String ss[] = line.split("="); mLoginInfo.put(ss[0], ss[1]); } } finally { in.close(); } updateMessage(R.string.request_market, null); apiRequest(); } catch (Exception e) { updateMessage(R.string.error_download, e.toString()); } } @SuppressWarnings("unused") private static String readString(InputStream is) { try { final int len = readInt32(is); final byte[] data = new byte[len]; is.read(data, 0, len); return new String(data); } catch (IOException e) { e.printStackTrace(); } return null; } private static int readInt32(InputStream is) { int n = 0; try { int b = is.read(); if (b <= 128) { return b; } return (b - 128) + readInt32(is) * 128; } catch (IOException e) { e.printStackTrace(); } return n; } private static void writeInt32(OutputStream os, int n) { try { for (int i = 0; i < 5 && n != 0; i++) { int b = n % 128; n >>= 7; if (n != 0) { b += 128; } os.write(b); } } catch (IOException e) { e.printStackTrace(); } } private static void writeString(OutputStream os, String s) { try { byte[] buf = s.getBytes(); writeInt32(os, buf.length); os.write(buf, 0, buf.length); } catch (IOException e) { e.printStackTrace(); } } private byte[] createRequest() { try { String packageName = PACKAGE_NAME; int baseLen; ByteArrayOutputStream os = new ByteArrayOutputStream(); os.write(10); writeString(os, mLoginInfo.get("Auth")); os.write(16); os.write(1); os.write(24); writeInt32(os, 2009011); os.write(34); writeString(os, mDeviceId); os.write(42); writeString(os, "passion:9"); os.write(50); writeString(os, "en"); os.write(58); writeString(os, "us"); os.write(66); writeString(os, "DoCoMo"); os.write(74); writeString(os, "DoCoMo"); os.write(82); writeString(os, "44010"); os.write(90); writeString(os, "44010"); baseLen = os.size() + 1 - 3; // reduce three bytes - one marker (10) plus 2 bytes for baseLen os.write(19); os.write(82); writeInt32(os, packageName.length() + 2); os.write(10); writeString(os, packageName); os.write(20); os.flush(); os.close(); ByteArrayOutputStream request = new ByteArrayOutputStream(); request.write(10); writeInt32(request, baseLen + 2); request.write(os.toByteArray(), 0, os.size()); return request.toByteArray(); } catch (Exception e) { e.printStackTrace(); } return null; } private void apiRequest() { try { final HttpPost request = new HttpPost(URL_DOWNLOAD); request.addHeader("Content-type", "application/x-www-form-urlencoded"); request.addHeader("User-Agent", "Android-Market/2"); request.addHeader("Cookie", "ANDROIDSECURE=" + mLoginInfo.get("Auth")); final List<NameValuePair> params = new ArrayList<NameValuePair>(4); params.add(new BasicNameValuePair("version", String.valueOf(PROTOCOL_VERSION))); String request64 = Base64.encodeToString(createRequest(), Base64.URL_SAFE); params.add(new BasicNameValuePair("request", request64)); request.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8)); final HttpResponse response = mClient.execute(request); final HttpEntity entity = response.getEntity(); if (response.getStatusLine().getStatusCode() >= 400) { updateMessage(R.string.error_download, response.getStatusLine().getReasonPhrase()); return; } String downloadUrl = null; String marketDa = null; final InputStream in = entity.getContent(); final GZIPInputStream zis = new GZIPInputStream(new BufferedInputStream(in)); // Response is in ProtBuf format. But use a easy way to extract string here final String resString = streamToString(zis); Matcher m = PATTERN_URL.matcher(resString); if (m.find()) { downloadUrl = m.group(); } m = PATTERN_MARKETDA.matcher(resString); if (m.find()) { marketDa = m.group(1); } if (downloadUrl == null || marketDa == null) { updateMessage(R.string.error_download, "Missing URL or MarketDA in response"); return; } updateMessage(R.string.download_package, null); download(downloadUrl, marketDa); } catch (Exception e) { updateMessage(R.string.error_download, e.toString()); } } private void download(String urlstr, String marketDa) { try { final HttpGet request = new HttpGet(urlstr); request.addHeader("User-Agent", "Android-Market/2"); request.addHeader("Cookie", "MarketDA=" + marketDa); final HttpResponse response = mClient.execute(request); if (response.getStatusLine().getStatusCode() != 200) { updateMessage(R.string.error_download, response.getStatusLine().getReasonPhrase()); return; } final File file = new File(Environment.getExternalStorageDirectory(), PACKAGE_NAME + ".apk"); if (file.exists()) { file.delete(); } final FileOutputStream out = new FileOutputStream(file); final InputStream in = response.getEntity().getContent(); long total = response.getEntity().getContentLength(); long len = total; try { final byte[] buf = new byte[BUFSIZE]; while (len > 0) { int rsz = in.read(buf); if (rsz < 0) { break; } out.write(buf, 0, rsz); len -= rsz; updateProgress((int) (100 * (total - len) / total)); } if (len != 0) { updateMessage(R.string.error_download, "Insufficient Response"); return; } updateMessage(R.string.install_package, null); final Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive"); startActivity(intent); finish(); } finally { in.close(); out.flush(); out.close(); } } catch (Exception e) { updateMessage(R.string.error_download, e.toString()); } } }