blob: 04d12fc4980d812e519cf4c0854c54da8c09db33 [file] [log] [blame]
/*
* 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.DownloadManager;
import android.app.DownloadManager.Request;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.app.TaskStackBuilder;
import android.content.BroadcastReceiver;
import android.content.Context;
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.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.SystemClock;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.widget.Toast;
import com.fairphone.updater.data.UpdaterData;
import com.fairphone.updater.data.Version;
import com.fairphone.updater.data.VersionParserHelper;
import com.fairphone.updater.gappsinstaller.GappsInstallerHelper;
import com.fairphone.updater.tools.PrivilegeChecker;
import com.fairphone.updater.tools.RSAUtils;
import com.fairphone.updater.tools.Utils;
import com.stericson.RootTools.execution.CommandCapture;
import com.stericson.RootTools.execution.Shell;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.util.concurrent.TimeoutException;
public class UpdaterService extends Service
{
public static final String LAST_CONFIG_DOWNLOAD_IN_MS = "LAST_CONFIG_DOWNLOAD_IN_MS";
private static final int CONFIG_FILE_DOWNLOAD_TIMEOUT_MILLIS = 23500;
public static final String EXTRA_FORCE_CONFIG_FILE_DOWNLOAD = "FORCE_DOWNLOAD";
private static final String TAG = UpdaterService.class.getSimpleName();
public static final String PREFERENCE_LAST_CONFIG_DOWNLOAD_ID = "LastConfigDownloadId";
public static final String PREFERENCE_REINSTALL_GAPPS = "ReinstallGappsOmnStartUp";
public static final String PREFERENCE_CONFIG_MD_5 = "CONFIG_MD5";
private DownloadManager mDownloadManager = null;
private DownloadBroadCastReceiver mDownloadBroadCastReceiver = null;
private static final int MAX_DOWNLOAD_RETRIES = 3;
private int mDownloadRetries;
private long mLatestFileDownloadId;
private boolean mInternetConnectionAvailable;
private SharedPreferences mSharedPreferences;
private final static long DOWNLOAD_GRACE_PERIOD_IN_MS = 24 /* hour */ * Utils.MINUTES_IN_HOUR /* minute */ * Utils.SECONDS_IN_MINUTE /* second */ * 1000 /* millisecond */;
private BroadcastReceiver networkStateReceiver;
@Override
public int onStartCommand(Intent intent, int flags, int startId)
{
// remove the logs
clearDataLogs();
if(Utils.isDeviceUnsupported(getApplicationContext())){
stopSelf();
return START_NOT_STICKY;
}
mSharedPreferences = getApplicationContext().getSharedPreferences(FairphoneUpdater.FAIRPHONE_UPDATER_PREFERENCES, MODE_PRIVATE);
mLatestFileDownloadId = mSharedPreferences.getLong(PREFERENCE_LAST_CONFIG_DOWNLOAD_ID, 0);
setupDownloadManager();
setupConnectivityMonitoring();
if(Utils.isInternetEnabled(getApplicationContext()))
{
downloadConfigFile(intent != null && intent.getBooleanExtra(EXTRA_FORCE_CONFIG_FILE_DOWNLOAD, false));
}
// setup the gapps installer
GappsInstallerHelper.checkGappsAreInstalled(getApplicationContext());
runInstallationDisclaimer(getApplicationContext());
return Service.START_STICKY;
}
private static void runInstallationDisclaimer(Context context)
{
SharedPreferences sharedPreferences = context.getApplicationContext().getSharedPreferences(FairphoneUpdater.FAIRPHONE_UPDATER_PREFERENCES, MODE_PRIVATE);
if (sharedPreferences.getBoolean(PREFERENCE_REINSTALL_GAPPS, true) && !UpdaterData.getInstance().isAppStoreListEmpty())
{
if(!GappsInstallerHelper.areGappsInstalled()){
showReinstallAlert(context);
}
Editor editor = sharedPreferences.edit();
editor.putBoolean(PREFERENCE_REINSTALL_GAPPS, false);
editor.apply();
}
}
private static void showReinstallAlert(Context context)
{
if ( FairphoneUpdater.BETA_MODE_ENABLED )
{
return;
}
NotificationManager mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
//Intent notificationIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(context.getResources().getString(R.string.supportAppStoreUrl)));
Intent notificationIntent = new Intent(context, FairphoneUpdater.class);
notificationIntent.setAction(GappsInstallerHelper.EXTRA_START_GAPPS_INSTALL);
PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context).setSmallIcon(R.drawable.updater_white)
.setContentTitle(context.getResources().getString(R.string.app_full_name))
.setContentText(context.getResources().getString(R.string.appStoreReinstall))
.setAutoCancel(true)
.setDefaults(Notification.DEFAULT_SOUND)
.setContentIntent(contentIntent);
mNotificationManager.notify(0, mBuilder.build());
}
private void downloadConfigFile(boolean forceDownload)
{
long now = System.currentTimeMillis();
long last_download = mSharedPreferences.getLong(LAST_CONFIG_DOWNLOAD_IN_MS, 0L);
if( forceDownload || now > (last_download + DOWNLOAD_GRACE_PERIOD_IN_MS) ) {
Log.i(TAG, "Downloading updater configuration file.");
// remove the old file if its still there for some reason
removeLatestFileDownload(getApplicationContext());
// start the download of the latest file
startDownloadLatest();
mSharedPreferences.edit().putLong(LAST_CONFIG_DOWNLOAD_IN_MS, now).apply();
}
}
// --Commented out by Inspection START (06/02/2015 12:27):
// public void updateGoogleAppsIntallerWidgets()
// {
// AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);
// int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(this, GoogleAppsInstallerWidget.class));
// if (appWidgetIds.length > 0)
// {
// new GoogleAppsInstallerWidget().onUpdate(this, appWidgetManager, appWidgetIds);
// }
// }
// --Commented out by Inspection STOP (06/02/2015 12:27)
private static void clearDataLogs()
{
if (PrivilegeChecker.isPrivilegedApp()) {
clearDataLogsPrivileged();
} else {
clearDataLogsUnprivileged();
}
}
private static void clearDataLogsPrivileged()
{
try
{
Log.d(TAG, "Clearing dump log data...");
Process p = Runtime.getRuntime().exec("rm /data/log_other_mode/*_log");
try
{
p.waitFor();
} catch (InterruptedException e)
{
e.printStackTrace();
}
} catch (IOException e)
{
Log.d(TAG, "Clearing dump log data failed: " + e.getLocalizedMessage());
}
}
private static void clearDataLogsUnprivileged()
{
try
{
Log.d(TAG, "Clearing dump log data...");
Shell.runCommand(new CommandCapture(0, "rm /data/log_other_mode/*_log"));
} catch (IOException | TimeoutException e)
{
Log.d(TAG, "Clearing dump log data failed: " + e.getLocalizedMessage());
}
}
@Override
public IBinder onBind(Intent intent)
{
return null;
}
void startDownloadLatest()
{
Resources resources = getApplicationContext().getResources();
String downloadLink = getConfigDownloadLink(getApplicationContext());
// set the download for the latest version on the download manager
Request request = createDownloadRequest(downloadLink, resources.getString(R.string.configFilename) + resources.getString(R.string.config_zip));
request.setNotificationVisibility(Request.VISIBILITY_HIDDEN);
if (request != null && mDownloadManager != null)
{
// Allow download over mobile data and Wi-Fi
request.setAllowedNetworkTypes(Request.NETWORK_MOBILE|Request.NETWORK_WIFI);
//Guarantee that only we have only one download
long oldDownloadId = mSharedPreferences.getLong(PREFERENCE_LAST_CONFIG_DOWNLOAD_ID, 0);
if(oldDownloadId != 0){
mDownloadManager.remove(oldDownloadId);
saveLatestDownloadId(0);
}
mLatestFileDownloadId = mDownloadManager.enqueue(request);
saveLatestDownloadId(mLatestFileDownloadId);
final long currentId = mLatestFileDownloadId;
// Cancel download if it is stuck since DownloadManager doesn't seem able to do it.
new Handler().postAtTime(new Runnable() {
@Override
public void run() {
onDownloadStatus(currentId, new Runnable() {
@Override
public void run() {
Log.w(TAG, "Configuration file download timed out");
mDownloadManager.remove(currentId);
mSharedPreferences.edit().remove(LAST_CONFIG_DOWNLOAD_IN_MS).apply();
}
});
}
}, SystemClock.uptimeMillis() + CONFIG_FILE_DOWNLOAD_TIMEOUT_MILLIS);
}
else
{
Log.e(TAG, "Invalid request for link " + downloadLink);
Intent i = new Intent(FairphoneUpdater.FAIRPHONE_UPDATER_CONFIG_DOWNLOAD_FAILED);
i.putExtra(FairphoneUpdater.FAIRPHONE_UPDATER_CONFIG_DOWNLOAD_LINK, downloadLink);
sendBroadcast(i);
}
}
private void saveLatestDownloadId(long id)
{
mLatestFileDownloadId = id;
Editor editor = mSharedPreferences.edit();
editor.putLong(PREFERENCE_LAST_CONFIG_DOWNLOAD_ID, id);
editor.commit();
}
private String getConfigDownloadLink(Context context)
{
Resources resources = context.getResources();
StringBuilder sb = new StringBuilder();
String download_url = mSharedPreferences.getString(FairphoneUpdater.PREFERENCE_OTA_DOWNLOAD_URL, getResources().getString(R.string.downloadUrl));
sb.append(download_url);
sb.append(Build.MODEL.replaceAll("\\s", ""));
sb.append(Utils.getPartitionDownloadPath(resources));
sb.append("/");
sb.append(resources.getString(R.string.configFilename));
sb.append(resources.getString(R.string.config_zip));
addUrlParameters(context, sb);
String downloadLink = sb.toString();
Log.d(TAG, "Download link: " + downloadLink);
return downloadLink;
}
private static void addUrlParameters(Context context, StringBuilder sb)
{
sb.append("?");
Version currentVersion = VersionParserHelper.getDeviceVersion(context.getApplicationContext());
if (currentVersion != null) {
try {
final String defaultCharset = Charset.defaultCharset().displayName();
String modelWithoutSpaces = Build.MODEL.replaceAll("\\s", "");
if(modelWithoutSpaces.startsWith(context.getResources().getString(R.string.FP1Model))) {
sb.append("&b_n=").append(URLEncoder.encode(currentVersion.getBuildNumber(), defaultCharset));
}
sb.append("&ota_v_n=").append(URLEncoder.encode(String.valueOf(currentVersion.getId()), defaultCharset));
sb.append("&beta=").append(FairphoneUpdater.BETA_MODE_ENABLED ? BetaEnabler.BETA_ENABLED : BetaEnabler.BETA_DISABLED);
sb.append("&dev=").append(FairphoneUpdater.DEV_MODE_ENABLED ? "1" : "0");
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Failed to add extra info on update request: "+e.getLocalizedMessage());
}
}
}
private static void setNotification(Context currentContext)
{
if ( FairphoneUpdater.BETA_MODE_ENABLED )
{
return;
}
Context context = currentContext.getApplicationContext();
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context).setSmallIcon(R.drawable.updater_white)
.setContentTitle(context.getResources().getString(R.string.app_full_name))
.setContentText(context.getResources().getString(R.string.fairphone_update_message));
Intent resultIntent = new Intent(context, FairphoneUpdater.class);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
stackBuilder.addParentStack(FairphoneUpdater.class);
stackBuilder.addNextIntent(resultIntent);
PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(resultPendingIntent);
Notification notificationWhileRunnig = builder.build();
// Add notification
manager.notify(0, notificationWhileRunnig);
}
private Request createDownloadRequest(String url, String fileName)
{
Resources resources = getApplicationContext().getResources();
Request request;
try
{
request = new Request(Uri.parse(url));
final File externalStoragePublicDirectory = Environment.getExternalStoragePublicDirectory(Environment.getExternalStorageDirectory() + resources.getString(R.string.updaterFolder));
final boolean notMkDirs = !externalStoragePublicDirectory.mkdirs();
if(notMkDirs && !externalStoragePublicDirectory.exists()) {
throw new Exception("Couldn't create updater dir structures.");
}
request.setDestinationInExternalPublicDir(resources.getString(R.string.updaterFolder), fileName);
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
request.setAllowedOverRoaming(false);
request.setTitle(resources.getString(R.string.fairphone_update_message_title));
} catch (Exception e)
{
Log.e(TAG, "Error creating request: " + e.getMessage());
request = null;
}
return request;
}
private void setupConnectivityMonitoring()
{
if (networkStateReceiver == null) {
// Check current connectivity status
mInternetConnectionAvailable = Utils.isInternetEnabled(getApplicationContext());
// Setup monitoring for future connectivity status changes
networkStateReceiver = new BroadcastReceiver()
{
@Override
public void onReceive(Context context, Intent intent)
{
if (intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false)) {
Log.i(TAG, "Lost network connectivity.");
mInternetConnectionAvailable = false;
if (mLatestFileDownloadId != 0 && mDownloadManager != null)
{
onDownloadStatus(mLatestFileDownloadId, new Runnable() {
@Override
public void run() {
Log.d(TAG, "Removing pending download.");
mDownloadManager.remove(mLatestFileDownloadId);
saveLatestDownloadId(0);
}
});
}
}
else
{
int conn_type = intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, ConnectivityManager.TYPE_DUMMY);
if( conn_type == ConnectivityManager.TYPE_WIFI || conn_type == ConnectivityManager.TYPE_MOBILE) {
Log.i(TAG, "Network connectivity potentially available.");
if (!mInternetConnectionAvailable) {
downloadConfigFile(false);
}
mInternetConnectionAvailable = true;
}
}
}
};
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
registerReceiver(networkStateReceiver, filter);
}
}
private void setupDownloadManager()
{
if (mDownloadManager == null)
{
mDownloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
}
if (mDownloadBroadCastReceiver == null)
{
mDownloadBroadCastReceiver = new DownloadBroadCastReceiver();
getApplicationContext().registerReceiver(mDownloadBroadCastReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
}
private static void copyConfigToData(Context context) throws IOException {
Resources resources = context.getApplicationContext().getResources();
String targetPath = Environment.getExternalStorageDirectory() + resources.getString(R.string.updaterFolder);
FileInputStream inStream = new FileInputStream(targetPath + resources.getString(R.string.configFilename) + resources.getString(R.string.config_xml));
FileOutputStream outStream = context.openFileOutput(resources.getString(R.string.configFilename) + resources.getString(R.string.config_xml), MODE_PRIVATE);
FileChannel inChannel = inStream.getChannel();
FileChannel outChannel = outStream.getChannel();
inChannel.transferTo(0, inChannel.size(), outChannel);
inStream.close();
outStream.close();
}
private static void checkVersionValidation(Context context)
{
Version latestVersion = VersionParserHelper.getLatestVersion(context.getApplicationContext());
Version currentVersion = VersionParserHelper.getDeviceVersion(context.getApplicationContext());
if (latestVersion != null)
{
if (latestVersion.isNewerVersionThan(currentVersion))
{
setNotification(context);
}
}
runInstallationDisclaimer(context);
// to update the activity
Intent updateIntent = new Intent(FairphoneUpdater.FAIRPHONE_UPDATER_NEW_VERSION_RECEIVED);
context.sendBroadcast(updateIntent);
}
public static boolean readUpdaterData(Context context){
Version latestVersion = VersionParserHelper.getLatestVersion(context.getApplicationContext());
return latestVersion != null;
}
public static boolean updateUpdaterData(Context context)
{
boolean retVal = false;
Resources resources = context.getApplicationContext().getResources();
String targetPath = Environment.getExternalStorageDirectory() + resources.getString(R.string.updaterFolder);
String filePath = targetPath + resources.getString(R.string.configFilename) + resources.getString(R.string.config_zip);
File file = new File(filePath);
if (file.exists())
{
String md5sum = Utils.calculateMD5(file);
SharedPreferences sp = context.getApplicationContext().getSharedPreferences(FairphoneUpdater.FAIRPHONE_UPDATER_PREFERENCES, MODE_PRIVATE);
if(sp.getString(PREFERENCE_CONFIG_MD_5, "").equals(md5sum)){
retVal = true;
} else {
if (RSAUtils.checkFileSignature(context, filePath, targetPath)) {
try {
copyConfigToData(context);
checkVersionValidation(context);
retVal = true;
sp.edit().putString(PREFERENCE_CONFIG_MD_5, md5sum).apply();
} catch (IOException e) {
Log.e(TAG, "Failed to store configuration " + e.getLocalizedMessage());
retVal = false;
}
} else {
//Toast.makeText(context, resources.getString(R.string.invalid_signature_download_message), Toast.LENGTH_LONG).show();
final boolean notDeleted = !file.delete();
if(notDeleted) {
Log.d(TAG, "Unable to delete "+file.getAbsolutePath());
}
}
}
} else {
Log.w(TAG, "No file");
}
return retVal;
}
private void removeLatestFileDownload(Context context)
{
if (mLatestFileDownloadId != 0 && mDownloadManager != null)
{
mDownloadManager.remove(mLatestFileDownloadId);
saveLatestDownloadId(0);
}
VersionParserHelper.removeConfigFiles(context);
}
private boolean retryDownload(Context context)
{
Log.d(TAG, "Retry "+mDownloadRetries+" of "+MAX_DOWNLOAD_RETRIES);
// invalid file
boolean removeReceiver = true;
removeLatestFileDownload(context);
if (mDownloadRetries < MAX_DOWNLOAD_RETRIES)
{
startDownloadLatest();
mDownloadRetries++;
removeReceiver = false;
}
return removeReceiver;
}
private void onDownloadStatus(long id, Runnable ifRunning) {
Cursor cursor = mDownloadManager != null ? mDownloadManager.query(new DownloadManager.Query().setFilterById(id)) : null;
if (cursor != null && cursor.moveToFirst())
{
int status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS));
switch(status){
case DownloadManager.STATUS_PAUSED:
if (ifRunning != null) {
ifRunning.run();
}
break;
case DownloadManager.STATUS_PENDING:
if (ifRunning != null) {
ifRunning.run();
}
break;
case DownloadManager.STATUS_RUNNING:
if (ifRunning != null) {
ifRunning.run();
}
break;
case DownloadManager.STATUS_FAILED:
case DownloadManager.STATUS_SUCCESSFUL:
default:
break;
}
}
if (cursor != null)
{
cursor.close();
}
}
private class DownloadBroadCastReceiver extends BroadcastReceiver
{
@Override
public void onReceive(Context context, Intent intent)
{
boolean removeReceiver = false;
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(mLatestFileDownloadId);
Cursor cursor = mDownloadManager != null ? mDownloadManager.query(query) : null;
if (cursor != null && cursor.moveToFirst())
{
int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);
int status = cursor.getInt(columnIndex);
Resources resources = context.getApplicationContext().getResources();
switch (status)
{
case DownloadManager.STATUS_SUCCESSFUL:
{
Log.d(TAG, "Download successful.");
if (updateUpdaterData(context))
{
removeLatestFileDownload(context);
}
else
{
Toast.makeText(getApplicationContext(), resources.getString(R.string.invalid_signature_download_message), Toast.LENGTH_LONG).show();
removeReceiver = retryDownload(context);
}
break;
}
case DownloadManager.STATUS_FAILED:
{
Log.d(TAG, "Download failed.");
removeReceiver = retryDownload(context);
break;
}
default:
{
Log.d(TAG, "Status broadcast on mLatestFileDownloadId ("+mLatestFileDownloadId+"): "+ status);
}
}
}
if (cursor != null)
{
cursor.close();
}
if (removeReceiver)
{
Log.d(TAG, "Configuration download failed. Clearing grace period.");
mSharedPreferences.edit().remove(LAST_CONFIG_DOWNLOAD_IN_MS).apply();
}
}
}
}