/*
* Copyright (C) 2013 Fairphone Project
*
* 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.fairphone.updater;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.DownloadManager;
import android.app.DownloadManager.Request;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.PowerManager;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class FairphoneUpdater extends Activity {
public static final String FAIRPHONE_UPDATER_NEW_VERSION_RECEIVED = "FairphoneUpdater.NEW.VERSION.RECEIVED";
protected static final String PREFERENCE_NEW_VERSION_NAME = "PREFERENCE_NEW_VERSION_NAME";
protected static final String PREFERENCE_NEW_VERSION_NUMBER = "PREFERENCE_NEW_VERSION_NUMBER";
protected static final String PREFERENCE_NEW_VERSION_MD5_SUM = "PREFERENCE_NEW_VERSION_MD5_SUM";
protected static final String PREFERENCE_NEW_VERSION_URL = "PREFERENCE_NEW_VERSION_URL";
protected static final String PREFERENCE_NEW_VERSION_ANDROID = "PREFERENCE_NEW_VERSION_ANDROID";
private static final String ANDROID_LABEL = "Android ";
private static final String FAIRPHONE_LABEL = "Fairphone ";
private static final String TAG = FairphoneUpdater.class.getSimpleName();
private static final String PREFERENCE_CURRENT_UPDATER_STATE = "CurrentUpdaterState";
private static final String PREFERENCE_DOWNLOAD_ID = "LatestUpdateDownloadId";
public static final String FAIRPHONE_UPDATER_PREFERENCES = "FairphoneUpdaterPreferences";
public static enum UpdaterState {
NORMAL, DOWNLOAD, PREINSTALL
};
private Version mDeviceVersion;
private Version mLatestVersion;
private UpdaterState mCurrentState;
private SharedPreferences mSharedPreferences;
// views
private TextView mViewCurrentVersionTitle;
private TextView mViewCurrentVersionText;
private TextView mViewUpdateVersionTitle;
private TextView mViewUpdateVersionText;
private TextView mViewMessageText;
private Button mViewUpdateButton;
private LinearLayout mLatestGroupLla;
private DownloadManager mDownloadManager;
private DownloadBroadCastReceiver mDownloadBroadCastReceiver;
private long mLatestUpdateDownloadId;
private BroadcastReceiver newVersionbroadcastReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fairphone_updater);
setupLayout();
mSharedPreferences = getSharedPreferences(
FAIRPHONE_UPDATER_PREFERENCES, MODE_PRIVATE);
// get system data
mDeviceVersion = VersionParserHelper.getDeviceVersion(this);
mLatestVersion = getLastestVersion();
// check current state
mCurrentState = getCurrentUpdaterState();
setupInstallationReceivers();
setupBroadcastReceiver();
// TODO : remove this
Intent i = new Intent(this, UpdaterService.class);
startService(i);
}
protected void setupBroadcastReceiver() {
newVersionbroadcastReceiver = new BroadcastReceiver(){
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (FairphoneUpdater.FAIRPHONE_UPDATER_NEW_VERSION_RECEIVED.equals(action)) {
mLatestVersion = getLastestVersion();
updateNewVersionLayout();
}
}
};
}
private Version getLastestVersion() {
Version latest = null;
String newVersionName = mSharedPreferences.getString(
PREFERENCE_NEW_VERSION_NAME, null);
String number = mSharedPreferences.getString(
PREFERENCE_NEW_VERSION_NUMBER, null);
String url = mSharedPreferences.getString(PREFERENCE_NEW_VERSION_URL,
null);
String md5 = mSharedPreferences.getString(
PREFERENCE_NEW_VERSION_MD5_SUM, null);
String android = mSharedPreferences.getString(
PREFERENCE_NEW_VERSION_ANDROID, null);
if (newVersionName != null && number != null && url != null
&& md5 != null && android != null) {
latest = new Version();
latest.setName(newVersionName);
latest.setNumber(number);
latest.setDownloadLink(url);
latest.setMd5Sum(md5);
latest.setAndroid(android);
}
return latest;
}
private void setupLayout() {
mViewCurrentVersionTitle = (TextView) findViewById(R.id.currentVersionTitleText);
mViewCurrentVersionTitle.setVisibility(View.VISIBLE);
mViewCurrentVersionText = (TextView) findViewById(R.id.currentVersionDescriptionText);
mViewCurrentVersionText.setVisibility(View.VISIBLE);
mViewUpdateVersionTitle = (TextView) findViewById(R.id.nextVersionTitleText);
mViewUpdateVersionText = (TextView) findViewById(R.id.nextVersionDescriptionText);
mViewUpdateButton = (Button) findViewById(R.id.newVersionUpdateButton);
mViewMessageText = (TextView) findViewById(R.id.messageText);
mViewUpdateButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mCurrentState == UpdaterState.NORMAL) {
startUpdateDownload();
} else if (mCurrentState == UpdaterState.PREINSTALL) {
startPreInstall();
}
}
});
mLatestGroupLla = (LinearLayout) findViewById(R.id.latestVersionGroup);
mLatestGroupLla.setVisibility(View.GONE);
}
public String getStringPreference(String key) {
return mSharedPreferences.getString(key, null);
}
public long getLongPreference(String key) {
return mSharedPreferences.getLong(key, 0);
}
public boolean getBooleanPreference(String key) {
return mSharedPreferences.getBoolean(key, false);
}
public void savePreference(String key, String value) {
Editor editor = mSharedPreferences.edit();
editor.putString(key, value);
editor.commit();
}
public void savePreference(String key, boolean value) {
Editor editor = mSharedPreferences.edit();
editor.putBoolean(key, value);
editor.commit();
}
public void savePreference(String key, long value) {
Editor editor = mSharedPreferences.edit();
editor.putLong(key, value);
editor.commit();
}
@Override
protected void onResume() {
super.onResume();
registerBroadCastReceiver();
// check current state
mCurrentState = getCurrentUpdaterState();
mLatestVersion = VersionParserHelper.getLastestVersion(this);
if (mLatestVersion != null) {
if(!mLatestVersion.isNewerVersionThan(mDeviceVersion)){
mLatestVersion.deleteFromSharedPreferences(this);
mLatestVersion = null;
}
}
mViewCurrentVersionTitle.setText(mDeviceVersion.getName());
mViewCurrentVersionText.setText(FAIRPHONE_LABEL
+ mDeviceVersion.getNumber() + "\n" + ANDROID_LABEL
+ mDeviceVersion.getAndroid());
setupState(mCurrentState);
}
private void setupState(UpdaterState state) {
switch (state) {
case NORMAL:
setupNormalState();
break;
case DOWNLOAD:
setupDownloadState();
break;
case PREINSTALL:
setupPreInstallState();
break;
}
}
private void changeState(UpdaterState newState) {
mCurrentState = newState;
Editor editor = mSharedPreferences.edit();
editor.putString(PREFERENCE_CURRENT_UPDATER_STATE, mCurrentState.name());
editor.commit();
setupState(mCurrentState);
}
@Override
protected void onPause() {
super.onPause();
unregisterBroadCastReceiver();
}
private void setupNormalState() {
if (mLatestUpdateDownloadId != 0) {
// residue download ID
mDownloadManager.remove(mLatestUpdateDownloadId);
mLatestUpdateDownloadId = 0;
savePreference(PREFERENCE_DOWNLOAD_ID, mLatestUpdateDownloadId);
}
// check to see if there is a new version to install
updateNewVersionLayout();
}
protected void updateNewVersionLayout() {
if (mLatestVersion != null) {
mLatestGroupLla.setVisibility(View.VISIBLE);
mViewUpdateButton.setVisibility(View.VISIBLE);
mViewUpdateVersionTitle.setText(mLatestVersion.getName());
mViewUpdateVersionText.setText(FAIRPHONE_LABEL
+ mLatestVersion.getNumber() + "\n" + ANDROID_LABEL
+ mLatestVersion.getAndroid());
mViewMessageText.setVisibility(View.GONE);
mViewUpdateButton.setText(getResources().getString(
R.string.installVersion));
} else {
mLatestGroupLla.setVisibility(View.GONE);
}
}
private UpdaterState getCurrentUpdaterState() {
String currentState = getStringPreference(PREFERENCE_CURRENT_UPDATER_STATE);
if (currentState == null || currentState.isEmpty()) {
currentState = UpdaterState.NORMAL.name();
Editor editor = mSharedPreferences.edit();
editor.putString(currentState, currentState);
editor.commit();
}
return UpdaterState.valueOf(currentState);
}
private static String getVersionDownloadPath(Version version) {
return Environment.getExternalStorageDirectory()
+ VersionParserHelper.UPDATER_FOLDER
+ VersionParserHelper.getNameFromVersion(version);
}
// ************************************************************************************
// PRE INSTALL
// ************************************************************************************
private void setupPreInstallState() {
// the latest version data must exist
if (mLatestVersion != null) {
mViewUpdateVersionTitle.setText(mLatestVersion.getName());
mViewUpdateVersionText.setText(FAIRPHONE_LABEL
+ mLatestVersion.getNumber() + "\n" + ANDROID_LABEL
+ mLatestVersion.getAndroid());
// check the md5 of the file
File file = new File(getVersionDownloadPath(mLatestVersion));
if (file.exists()) {
if (FairphoneUpdater.checkMD5(mLatestVersion.getMd5Sum(), file)) {
mLatestGroupLla.setVisibility(View.VISIBLE);
mViewMessageText.setText(getResources().getString(
R.string.messageReadyToInstall));
mViewUpdateButton.setText(getResources().getString(
R.string.rebootDevice));
mViewUpdateButton.setVisibility(View.VISIBLE);
mViewMessageText.setVisibility(View.VISIBLE);
return;
} else {
mDownloadManager.remove(mLatestUpdateDownloadId);
mLatestUpdateDownloadId = 0;
savePreference(PREFERENCE_DOWNLOAD_ID,
mLatestUpdateDownloadId);
Toast.makeText(
this,
getResources().getString(
R.string.invalidDownloadMessage),
Toast.LENGTH_SHORT).show();
}
}
}
// remove the updater directory
File fileDir = new File(Environment.getExternalStorageDirectory()
+ VersionParserHelper.UPDATER_FOLDER);
fileDir.delete();
// else if the perfect case does not happen, reset the download
changeState(UpdaterState.NORMAL);
}
private void startPreInstall() {
// set the command for the recovery
Process p;
try {
p = Runtime.getRuntime().exec("su");
DataOutputStream os = new DataOutputStream(p.getOutputStream());
os.writeBytes("rm -f /cache/recovery/command\n");
os.writeBytes("rm -f /cache/recovery/extendedcommand\n");
os.writeBytes("echo '--wipe_cache' >> /cache/recovery/command\n");
os.writeBytes("echo '--update_package=/"
+ VersionParserHelper.RECOVERY_PATH
+ VersionParserHelper.UPDATER_FOLDER
+ VersionParserHelper.getNameFromVersion(mLatestVersion)
+ "' >> /cache/recovery/command\n");
os.writeBytes("sync\n");
os.writeBytes("exit\n");
os.flush();
p.waitFor();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// reboot the device into recovery
((PowerManager) getSystemService(POWER_SERVICE)).reboot("recovery");
}
// ************************************************************************************
// DOWNLOAD UPDATE
// ************************************************************************************
private void startUpdateDownload() {
// use only on WiFi
if (isWiFiEnabled()) {
// set the download for the latest version on the download manager
String fileName = VersionParserHelper
.getNameFromVersion(mLatestVersion);
Request request = createDownloadRequest(
mLatestVersion.getDownloadLink(), fileName,
mLatestVersion.getName() + " FP Update");
mLatestUpdateDownloadId = mDownloadManager.enqueue(request);
// save it on the shared preferences
savePreference(PREFERENCE_DOWNLOAD_ID, mLatestUpdateDownloadId);
// change state to download
changeState(UpdaterState.DOWNLOAD);
} else {
Resources resources = this.getResources();
AlertDialog.Builder disclaimerDialog = new AlertDialog.Builder(this);
disclaimerDialog.setTitle(resources
.getString(R.string.wifiDiscaimerTitle));
// Setting Dialog Message
disclaimerDialog.setMessage(resources
.getString(R.string.wifiDiscaimerMessage));
disclaimerDialog.setPositiveButton(
resources.getString(android.R.string.ok),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
// do nothing, since the state is still the same
}
});
disclaimerDialog.create();
disclaimerDialog.show();
}
}
private Request createDownloadRequest(String url, String fileName,
String downloadTitle) {
Request request = new Request(Uri.parse(url));
Environment.getExternalStoragePublicDirectory(
Environment.getExternalStorageDirectory()
+ VersionParserHelper.UPDATER_FOLDER).mkdirs();
request.setDestinationInExternalPublicDir(
VersionParserHelper.UPDATER_FOLDER, fileName);
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
request.setAllowedOverRoaming(false);
request.setTitle(downloadTitle);
return request;
}
private boolean isWiFiEnabled() {
ConnectivityManager manager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
boolean isWifi = manager.getNetworkInfo(ConnectivityManager.TYPE_WIFI)
.isConnectedOrConnecting();
return isWifi;
}
private void setupDownloadState() {
// setup the download state views
if (mLatestVersion == null) {
// we don't have the lastest.xml so get back to initial state
File updateDir = new File(Environment.getExternalStorageDirectory()
+ VersionParserHelper.UPDATER_FOLDER);
updateDir.delete();
changeState(UpdaterState.NORMAL);
return;
}
mViewUpdateVersionTitle.setText(mLatestVersion.getName());
mViewUpdateVersionText.setText(FAIRPHONE_LABEL
+ mLatestVersion.getNumber() + "\n" + ANDROID_LABEL
+ mLatestVersion.getAndroid());
// if there is a download ID on the shared preferences
if (mLatestUpdateDownloadId == 0) {
mLatestUpdateDownloadId = getLongPreference(PREFERENCE_DOWNLOAD_ID);
// invalid download Id
if (mLatestUpdateDownloadId == 0) {
changeState(UpdaterState.NORMAL);
return;
}
}
mLatestGroupLla.setVisibility(View.VISIBLE);
mViewUpdateButton.setVisibility(View.GONE);
mViewMessageText.setVisibility(View.VISIBLE);
mViewMessageText.setText(getResources().getString(
R.string.downloadMessage));
updateDownloadFile();
}
private void updateDownloadFile() {
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(mLatestUpdateDownloadId);
Cursor cursor = mDownloadManager.query(query);
if (cursor.moveToFirst()) {
int columnIndex = cursor
.getColumnIndex(DownloadManager.COLUMN_STATUS);
int status = cursor.getInt(columnIndex);
switch (status) {
case DownloadManager.STATUS_SUCCESSFUL:
changeState(UpdaterState.PREINSTALL);
break;
case DownloadManager.STATUS_RUNNING:
break;
case DownloadManager.STATUS_FAILED:
case DownloadManager.STATUS_PAUSED:
default:
changeState(UpdaterState.NORMAL);
mLatestUpdateDownloadId = 0;
savePreference(PREFERENCE_DOWNLOAD_ID, mLatestUpdateDownloadId);
break;
}
} else {
changeState(UpdaterState.NORMAL);
}
cursor.close();
}
private void setupInstallationReceivers() {
mDownloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
mDownloadBroadCastReceiver = new DownloadBroadCastReceiver();
}
private void registerBroadCastReceiver() {
registerReceiver(mDownloadBroadCastReceiver, new IntentFilter(
DownloadManager.ACTION_DOWNLOAD_COMPLETE));
registerReceiver(newVersionbroadcastReceiver, new IntentFilter(FairphoneUpdater.FAIRPHONE_UPDATER_NEW_VERSION_RECEIVED));
}
private void unregisterBroadCastReceiver() {
unregisterReceiver(mDownloadBroadCastReceiver);
unregisterReceiver(newVersionbroadcastReceiver);
}
private class DownloadBroadCastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (mLatestUpdateDownloadId == 0) {
mLatestUpdateDownloadId = getLongPreference(PREFERENCE_DOWNLOAD_ID);
}
updateDownloadFile();
}
}
// **************************************************************************************************************
// HELPERS
// **************************************************************************************************************
public static boolean checkMD5(String md5, File updateFile) {
if (!updateFile.exists()) {
return false;
}
if (md5 == null || md5.equals("") || updateFile == null) {
Log.e(TAG, "MD5 String NULL or UpdateFile NULL");
return false;
}
String calculatedDigest = calculateMD5(updateFile);
if (calculatedDigest == null) {
Log.e(TAG, "calculatedDigest NULL");
return false;
}
return calculatedDigest.equalsIgnoreCase(md5);
}
public static String calculateMD5(File updateFile) {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "Exception while getting Digest", e);
return null;
}
InputStream is;
try {
is = new FileInputStream(updateFile);
} catch (FileNotFoundException e) {
Log.e(TAG, "Exception while getting FileInputStream", e);
return null;
}
byte[] buffer = new byte[8192];
int read;
try {
while ((read = is.read(buffer)) > 0) {
digest.update(buffer, 0, read);
}
byte[] md5sum = digest.digest();
BigInteger bigInt = new BigInteger(1, md5sum);
String output = bigInt.toString(16);
// Fill to 32 chars
output = String.format("%32s", output).replace(' ', '0');
return output;
} catch (IOException e) {
throw new RuntimeException("Unable to process file for MD5", e);
} finally {
try {
is.close();
} catch (IOException e) {
Log.e(TAG, "Exception on closing MD5 input stream", e);
}
}
}
}