Java tutorial
/* Copyright (c) 2011, BogDan Vatra <bog_dan_ro@yahoo.com> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kde.necessitas.ministro; import java.io.BufferedReader; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Enumeration; import java.util.concurrent.Semaphore; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.apache.http.client.ClientProtocolException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import android.app.Activity; import android.app.AlertDialog; import android.app.NotificationManager; import android.app.ProgressDialog; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.SharedPreferences; import android.content.res.Configuration; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.os.StatFs; import android.provider.Settings; import android.util.Log; public class MinistroActivity extends Activity { private static final int CONNECTION_TIMEOUT = 20000; // 20 seconds for connection timeout private static final int READ_TIMEOUT = 10000; // 10 seconds for read timeout public native static int nativeChmode(String filepath, int mode); private static final String DOMAIN_NAME = "https://files.kde.org/necessitas/ministro/android/necessitas/"; private String[] m_modules; private int m_id = -1; private String m_qtLibsRootPath; private WakeLock m_wakeLock; private void checkNetworkAndDownload(final boolean update) { if (isOnline(this)) new CheckLibraries().execute(update); else { AlertDialog.Builder builder = new AlertDialog.Builder(MinistroActivity.this); builder.setMessage(getResources().getString(R.string.ministro_network_access_msg)); builder.setCancelable(true); builder.setNeutralButton(getResources().getString(R.string.settings_msg), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { final ProgressDialog m_dialog = ProgressDialog.show(MinistroActivity.this, null, getResources().getString(R.string.wait_for_network_connection_msg), true, true, new DialogInterface.OnCancelListener() { public void onCancel(DialogInterface dialog) { finishMe(); } }); getApplication().registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (isOnline(MinistroActivity.this)) { try { getApplication().unregisterReceiver(this); } catch (Exception e) { e.printStackTrace(); } runOnUiThread(new Runnable() { public void run() { m_dialog.dismiss(); new CheckLibraries().execute(update); } }); } } }, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); try { startActivity(new Intent(Settings.ACTION_WIRELESS_SETTINGS)); } catch (Exception e) { e.printStackTrace(); try { startActivity(new Intent(Settings.ACTION_WIFI_SETTINGS)); } catch (Exception e1) { e1.printStackTrace(); } } dialog.dismiss(); } }); builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }); builder.setOnCancelListener(new DialogInterface.OnCancelListener() { public void onCancel(DialogInterface dialog) { finishMe(); } }); AlertDialog alert = builder.create(); alert.show(); } } private AlertDialog m_distSpaceDialog = null; private final int freeSpaceCode = 0xf3ee500; private Semaphore m_diskSpaceSemaphore = new Semaphore(0); private boolean checkFreeSpace(final long size) throws InterruptedException { final StatFs stat = new StatFs(m_qtLibsRootPath); if (stat.getBlockSize() * stat.getAvailableBlocks() < size) { runOnUiThread(new Runnable() { public void run() { AlertDialog.Builder builder = new AlertDialog.Builder(MinistroActivity.this); builder.setMessage(getResources().getString(R.string.ministro_disk_space_msg, (size - (stat.getBlockSize() * stat.getAvailableBlocks())) / 1024 + "Kb")); builder.setCancelable(true); builder.setNeutralButton(getResources().getString(R.string.settings_msg), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { try { startActivityForResult( new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS), freeSpaceCode); } catch (Exception e) { e.printStackTrace(); try { startActivityForResult( new Intent(Settings.ACTION_MANAGE_ALL_APPLICATIONS_SETTINGS), freeSpaceCode); } catch (Exception e1) { e1.printStackTrace(); } } } }); builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.dismiss(); m_diskSpaceSemaphore.release(); } }); builder.setOnCancelListener(new DialogInterface.OnCancelListener() { public void onCancel(DialogInterface dialog) { dialog.dismiss(); m_diskSpaceSemaphore.release(); } }); m_distSpaceDialog = builder.create(); m_distSpaceDialog.show(); } }); m_diskSpaceSemaphore.acquire(); } else return true; return stat.getBlockSize() * stat.getAvailableBlocks() > size; } protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == freeSpaceCode) { m_diskSpaceSemaphore.release(); try { if (m_distSpaceDialog != null) { m_distSpaceDialog.dismiss(); m_distSpaceDialog = null; } } catch (Exception e) { e.printStackTrace(); } } } private ServiceConnection m_ministroConnection = new ServiceConnection() { public void onServiceConnected(ComponentName name, IBinder service) { if (getIntent().hasExtra("id")) m_id = getIntent().getExtras().getInt("id"); if (getIntent().hasExtra("modules")) { m_modules = getIntent().getExtras().getStringArray("modules"); AlertDialog.Builder builder = new AlertDialog.Builder(MinistroActivity.this); builder.setMessage(getResources().getString(R.string.download_app_libs_msg, getIntent().getExtras().getString("name"))).setCancelable(false) .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.dismiss(); checkNetworkAndDownload(false); } }).setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.cancel(); finishMe(); } }); AlertDialog alert = builder.create(); try { alert.show(); } catch (Exception e) { e.printStackTrace(); checkNetworkAndDownload(false); } } else checkNetworkAndDownload(true); } public void onServiceDisconnected(ComponentName name) { m_ministroConnection = null; } }; void finishMe() { if (-1 != m_id && null != MinistroService.instance()) MinistroService.instance().retrievalFinished(m_id); NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.cancelAll(); finish(); } private static URL getVersionUrl(Context c) throws MalformedURLException { return new URL(DOMAIN_NAME + MinistroService.getRepository(c) + "/" + android.os.Build.CPU_ABI + "/android-" + android.os.Build.VERSION.SDK_INT + "/versions.xml"); } private static URL getLibsXmlUrl(Context c, String version) throws MalformedURLException { return new URL(DOMAIN_NAME + MinistroService.getRepository(c) + "/" + android.os.Build.CPU_ABI + "/android-" + android.os.Build.VERSION.SDK_INT + "/libs-" + version + ".xml"); } public static boolean isOnline(Context c) { ConnectivityManager cm = (ConnectivityManager) c.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo netInfo = cm.getActiveNetworkInfo(); if (netInfo != null && netInfo.isConnectedOrConnecting()) return true; return false; } private static String deviceSupportedFeatures(String supportedFeatures) { if (null == supportedFeatures) return ""; String[] serverFeaturesList = supportedFeatures.trim().split(" "); String[] deviceFeaturesList = null; try { FileInputStream fstream = new FileInputStream("/proc/cpuinfo"); DataInputStream in = new DataInputStream(fstream); BufferedReader br = new BufferedReader(new InputStreamReader(in)); String strLine; while ((strLine = br.readLine()) != null) { if (strLine.startsWith("Features")) { deviceFeaturesList = strLine.substring(strLine.indexOf(":") + 1).trim().split(" "); break; } } br.close(); } catch (Exception e) { e.printStackTrace(); return ""; } String features = ""; for (String sfeature : serverFeaturesList) for (String dfeature : deviceFeaturesList) if (sfeature.equals(dfeature)) features += "_" + dfeature; return features; } public static double downloadVersionXmlFile(Context c, boolean checkOnly) { if (!isOnline(c)) return -1; try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document dom = null; Element root = null; URLConnection connection = getVersionUrl(c).openConnection(); connection.setConnectTimeout(CONNECTION_TIMEOUT); connection.setReadTimeout(READ_TIMEOUT); dom = builder.parse(connection.getInputStream()); root = dom.getDocumentElement(); root.normalize(); double version = Double.valueOf(root.getAttribute("latest")); if (MinistroService.instance().getVersion() >= version) return MinistroService.instance().getVersion(); if (checkOnly) return version; String supportedFeatures = null; if (root.hasAttribute("features")) supportedFeatures = root.getAttribute("features"); connection = getLibsXmlUrl(c, version + deviceSupportedFeatures(supportedFeatures)).openConnection(); File file = new File(MinistroService.instance().getVersionXmlFile()); file.delete(); FileOutputStream outstream = new FileOutputStream(MinistroService.instance().getVersionXmlFile()); InputStream instream = connection.getInputStream(); byte[] tmp = new byte[2048]; int downloaded; while ((downloaded = instream.read(tmp)) != -1) outstream.write(tmp, 0, downloaded); outstream.close(); MinistroService.instance().refreshLibraries(false); return version; } catch (ClientProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ParserConfigurationException e) { e.printStackTrace(); } catch (IllegalStateException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } return -1; } private class DownloadManager extends AsyncTask<Library, Integer, Long> { private ProgressDialog m_dialog = null; private String m_status = getResources().getString(R.string.start_downloading_msg); private int m_totalSize = 0, m_totalProgressSize = 0; @Override protected void onPreExecute() { m_dialog = new ProgressDialog(MinistroActivity.this); m_dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); m_dialog.setTitle(getResources().getString(R.string.downloading_qt_libraries_msg)); m_dialog.setMessage(m_status); m_dialog.setCancelable(true); m_dialog.setCanceledOnTouchOutside(false); m_dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { public void onCancel(DialogInterface dialog) { DownloadManager.this.cancel(false); finishMe(); } }); try { m_dialog.show(); } catch (Exception e) { e.printStackTrace(); m_dialog = null; } super.onPreExecute(); } private boolean DownloadItem(String url, String file, long size, String fileSha1) throws NoSuchAlgorithmException, MalformedURLException, IOException { for (int i = 0; i < 2; i++) { MessageDigest digester = MessageDigest.getInstance("SHA-1"); URLConnection connection = new URL(url).openConnection(); Library.mkdirParents(m_qtLibsRootPath, file, 1); String filePath = m_qtLibsRootPath + file; int progressSize = 0; try { FileOutputStream outstream = new FileOutputStream(filePath); InputStream instream = connection.getInputStream(); int downloaded; byte[] tmp = new byte[2048]; int oldProgress = -1; while ((downloaded = instream.read(tmp)) != -1) { if (isCancelled()) break; progressSize += downloaded; m_totalProgressSize += downloaded; digester.update(tmp, 0, downloaded); outstream.write(tmp, 0, downloaded); int progress = (int) (progressSize * 100 / size); if (progress != oldProgress) { publishProgress(progress, m_totalProgressSize); oldProgress = progress; } } String sha1 = Library.convertToHex(digester.digest()); if (sha1.equalsIgnoreCase(fileSha1)) { outstream.close(); nativeChmode(filePath, 0644); return true; } else Log.e("Ministro", "sha1 mismatch, the file:" + file + " will be removed, expected sha1:" + fileSha1 + " got sha1:" + sha1 + " file was downloaded from " + url); outstream.close(); File f = new File(filePath); f.delete(); } catch (Exception e) { e.printStackTrace(); File f = new File(filePath); f.delete(); } m_totalProgressSize -= progressSize; } return false; } void removeFile(String file) { File f = new File(m_qtLibsRootPath + file); f.delete(); } @Override protected Long doInBackground(Library... params) { try { for (int i = 0; i < params.length; i++) { m_totalSize += params[i].size; if (null != params[i].needs) for (int j = 0; j < params[i].needs.length; j++) m_totalSize += params[i].needs[j].size; } m_dialog.setMax(m_totalSize); if (!checkFreeSpace(m_totalSize)) return null; for (int i = 0; i < params.length; i++) { if (isCancelled()) break; synchronized (m_status) { m_status = params[i].name + " "; } publishProgress(0, m_totalProgressSize); if (!DownloadItem(params[i].url, params[i].filePath, params[i].size, params[i].sha1)) break; if (null != params[i].needs) for (int j = 0; j < params[i].needs.length; j++) { synchronized (m_status) { m_status = params[i].needs[j].name + " "; } publishProgress(0, m_totalProgressSize); if (!DownloadItem(params[i].needs[j].url, params[i].needs[j].filePath, params[i].needs[j].size, params[i].needs[j].sha1)) { for (int k = 0; k < j; k++) // remove previous neede files removeFile(params[i].needs[k].filePath); removeFile(params[i].filePath); // remove the parent break; } } } } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } return null; } @Override protected void onProgressUpdate(Integer... values) { try { if (m_dialog != null) { synchronized (m_status) { m_dialog.setMessage(m_status + values[0] + "%"); m_dialog.setProgress(values[1]); } } } catch (Exception e) { e.printStackTrace(); } super.onProgressUpdate(values); } @Override protected void onPostExecute(Long result) { super.onPostExecute(result); if (m_dialog != null) { m_dialog.dismiss(); m_dialog = null; } MinistroService.instance().refreshLibraries(false); finishMe(); } } private class CheckLibraries extends AsyncTask<Boolean, String, Double> { private ProgressDialog m_dialog = null; private final ArrayList<Library> newLibs = new ArrayList<Library>(); private String m_message; @Override protected void onPreExecute() { try { m_dialog = ProgressDialog.show(MinistroActivity.this, null, getResources().getString(R.string.checking_libraries_msg), true, true); } catch (Exception e) { e.printStackTrace(); m_dialog = null; } super.onPreExecute(); } @Override protected Double doInBackground(Boolean... update) { double version = 0.0; try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document dom = null; Element root = null; double oldVersion = MinistroService.instance().getVersion(); if (update[0] || MinistroService.instance().getVersion() < 0) version = downloadVersionXmlFile(MinistroActivity.this, false); else version = MinistroService.instance().getVersion(); SharedPreferences preferences = getSharedPreferences("Ministro", MODE_PRIVATE); // extract device look&feel if (!preferences.getString("CODENAME", "").equals(android.os.Build.VERSION.CODENAME) || !preferences.getString("INCREMENTAL", "").equals(android.os.Build.VERSION.INCREMENTAL) || !preferences.getString("RELEASE", "").equals(android.os.Build.VERSION.RELEASE) || !preferences.getString("MINISTRO_VERSION", "") .equals(getPackageManager().getPackageInfo(getPackageName(), 0).versionName) || !(new File(m_qtLibsRootPath + "style").exists())) { m_message = getResources().getString(R.string.extracting_look_n_feel_msg); publishProgress(m_message); new ExtractStyle(MinistroActivity.this, m_qtLibsRootPath + "style/"); SharedPreferences.Editor editor = preferences.edit(); editor.putString("MINISTRO_VERSION", getPackageManager().getPackageInfo(getPackageName(), 0).versionName); editor.commit(); } ArrayList<Library> libraries; if (update[0]) { if (oldVersion != version) libraries = MinistroService.instance().getDownloadedLibraries(); else return version; } else libraries = MinistroService.instance().getAvailableLibraries(); ArrayList<String> notFoundModules = new ArrayList<String>(); if (m_modules != null) MinistroService.instance().checkModules(m_modules, notFoundModules); dom = builder.parse(new FileInputStream(MinistroService.instance().getVersionXmlFile())); factory = DocumentBuilderFactory.newInstance(); builder = factory.newDocumentBuilder(); root = dom.getDocumentElement(); root.normalize(); // extract device root certificates if (!preferences.getString("CODENAME", "").equals(android.os.Build.VERSION.CODENAME) || !preferences.getString("INCREMENTAL", "").equals(android.os.Build.VERSION.INCREMENTAL) || !preferences.getString("RELEASE", "").equals(android.os.Build.VERSION.RELEASE)) { m_message = getResources().getString(R.string.extracting_SSL_msg); publishProgress(m_message); String environmentVariables = root.getAttribute("environmentVariables"); environmentVariables = environmentVariables.replaceAll("MINISTRO_PATH", ""); String environmentVariablesList[] = environmentVariables.split("\t"); for (int i = 0; i < environmentVariablesList.length; i++) { String environmentVariable[] = environmentVariablesList[i].split("="); if (environmentVariable[0].equals("MINISTRO_SSL_CERTS_PATH")) { String path = Library.mkdirParents(getFilesDir().getAbsolutePath(), environmentVariable[1], 0); Library.removeAllFiles(path); try { KeyStore ks = null; if (Build.VERSION.SDK_INT > 13) { ks = KeyStore.getInstance("AndroidCAStore"); ks.load(null, null); } else { ks = KeyStore.getInstance(KeyStore.getDefaultType()); String cacertsPath = System.getProperty("javax.net.ssl.trustStore"); if (null == cacertsPath) cacertsPath = "/system/etc/security/cacerts.bks"; FileInputStream instream = new FileInputStream(new File(cacertsPath)); ks.load(instream, null); } for (Enumeration<String> aliases = ks.aliases(); aliases.hasMoreElements();) { String aName = aliases.nextElement(); try { X509Certificate cert = (X509Certificate) ks.getCertificate(aName); if (null == cert) continue; String filePath = path + "/" + cert.getType() + "_" + cert.hashCode() + ".der"; FileOutputStream outstream = new FileOutputStream(new File(filePath)); byte buff[] = cert.getEncoded(); outstream.write(buff, 0, buff.length); outstream.close(); nativeChmode(filePath, 0644); } catch (KeyStoreException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } } catch (KeyStoreException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (CertificateException e) { e.printStackTrace(); } SharedPreferences.Editor editor = preferences.edit(); editor.putString("CODENAME", android.os.Build.VERSION.CODENAME); editor.putString("INCREMENTAL", android.os.Build.VERSION.INCREMENTAL); editor.putString("RELEASE", android.os.Build.VERSION.RELEASE); editor.commit(); break; } } } Node node = root.getFirstChild(); while (node != null) { if (node.getNodeType() == Node.ELEMENT_NODE) { Library lib = Library.getLibrary((Element) node, true); if (update[0]) { // check for updates for (int j = 0; j < libraries.size(); j++) if (libraries.get(j).name.equals(lib.name)) { newLibs.add(lib); break; } } else {// download missing libraries for (String module : notFoundModules) if (module.equals(lib.name)) { newLibs.add(lib); break; } } } // Workaround for an unbelievable bug !!! try { node = node.getNextSibling(); } catch (Exception e) { e.printStackTrace(); break; } } return version; } catch (ClientProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ParserConfigurationException e) { e.printStackTrace(); } catch (IllegalStateException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } return -1.; } @Override protected void onProgressUpdate(String... messages) { try { if (null != m_dialog) m_dialog.setMessage(messages[0]); } catch (Exception e) { e.printStackTrace(); } super.onProgressUpdate(messages); } @Override protected void onPostExecute(Double result) { try { if (null != m_dialog) { m_dialog.dismiss(); m_dialog = null; } } catch (Exception e) { e.printStackTrace(); } if (newLibs.size() > 0 && result > 0) { Library[] libs = new Library[newLibs.size()]; libs = newLibs.toArray(libs); new DownloadManager().execute(libs); } else finishMe(); super.onPostExecute(result); } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); m_qtLibsRootPath = getFilesDir().getAbsolutePath() + "/qt/"; File dir = new File(m_qtLibsRootPath); dir.mkdirs(); nativeChmode(m_qtLibsRootPath, 0755); bindService(new Intent("org.kde.necessitas.ministro.IMinistro"), m_ministroConnection, Context.BIND_AUTO_CREATE); PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); m_wakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "Ministro"); m_wakeLock.acquire(); } @Override protected void onDestroy() { super.onDestroy(); if (null != m_wakeLock) { m_wakeLock.release(); m_wakeLock = null; } unbindService(m_ministroConnection); } @Override public void onConfigurationChanged(Configuration newConfig) { //Avoid activity from being destroyed/created super.onConfigurationChanged(newConfig); } static { System.loadLibrary("ministro"); } }