Support different installation source. (Gplay + Fdroid)

Fix #82 #26
This commit is contained in:
Schoumi 2020-07-01 18:36:00 +02:00
parent 1bcdbca5e3
commit edbce7aab8
14 changed files with 173 additions and 67 deletions

View File

@ -77,9 +77,9 @@ public class MainActivity extends AppCompatActivity {
if(updatable instanceof ReportFragment) {
ApplicationViewModel model = ((ReportFragment) updatable).getModel();
if(model.versionName == null)
model.report = DatabaseManager.getInstance(MainActivity.this).getReportFor(model.packageName, model.versionCode);
model.report = DatabaseManager.getInstance(MainActivity.this).getReportFor(model.packageName, model.versionCode, model.source);
else
model.report = DatabaseManager.getInstance(MainActivity.this).getReportFor(model.packageName,model.versionName);
model.report = DatabaseManager.getInstance(MainActivity.this).getReportFor(model.packageName,model.versionName,model.source);
model.trackers = DatabaseManager.getInstance(MainActivity.this).getTrackers(model.report.trackers);
}
updatable.onUpdateComplete();

View File

@ -12,7 +12,6 @@ import org.eu.exodus_privacy.exodusprivacy.objects.ReportDisplay;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ReportViewModel extends BaseObservable {
@ -166,6 +165,11 @@ public class ReportViewModel extends BaseObservable {
return R.drawable.square_light_red;
}
@Bindable
public String getSource() {
return reportDisplay.source;
}
}

View File

@ -112,13 +112,13 @@ public class ApplicationListAdapter extends RecyclerView.Adapter {
return displayedApp;
}
class ApplicationEmptyViewHolder extends RecyclerView.ViewHolder{
static class ApplicationEmptyViewHolder extends RecyclerView.ViewHolder{
ApplicationEmptyViewHolder(View itemView) {
super(itemView);
}
}
class ApplicationListViewHolder extends RecyclerView.ViewHolder {
static class ApplicationListViewHolder extends RecyclerView.ViewHolder {
ApplicationViewModel viewModel;
AppItemBinding appItemBinding;
@ -145,6 +145,7 @@ public class ApplicationListAdapter extends RecyclerView.Adapter {
appItemBinding.appLogo.setImageDrawable(viewModel.icon);
appItemBinding.appName.setText(viewModel.label);
appItemBinding.source.setText(context.getString(R.string.source,viewModel.source));
long size = viewModel.requestedPermissions != null ? viewModel.requestedPermissions.length : 0;
appItemBinding.appPermissionNb.setText(String.valueOf(size));

View File

@ -25,4 +25,5 @@ public class ApplicationViewModel {
public CharSequence label;
public String installerPackageName;
public boolean isVisible;
public String source;
}

View File

@ -12,6 +12,7 @@ import org.eu.exodus_privacy.exodusprivacy.manager.DatabaseManager;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
class ComputeAppListTask extends AsyncTask<Void, Void, List<ApplicationViewModel>> {
@ -20,6 +21,7 @@ class ComputeAppListTask extends AsyncTask<Void, Void, List<ApplicationViewModel
}
private static final String gStore = "com.android.vending";
private static final String fdroid = "ord.fdroid.fdroid";
private WeakReference<PackageManager> packageManagerRef;
private WeakReference<DatabaseManager> databaseManagerRef;
@ -40,8 +42,8 @@ class ComputeAppListTask extends AsyncTask<Void, Void, List<ApplicationViewModel
List<ApplicationViewModel> vms = new ArrayList<>();
if(packageManager != null && databaseManager != null) {
List<PackageInfo> installedPackages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS);
applyStoreFilter(installedPackages, databaseManager, packageManager);
vms = convertPackagesToViewModels(installedPackages, databaseManager, packageManager);
vms = applyStoreFilter(installedPackages, databaseManager, packageManager);
convertPackagesToViewModels(vms, databaseManager, packageManager);
}
return vms;
}
@ -55,20 +57,22 @@ class ComputeAppListTask extends AsyncTask<Void, Void, List<ApplicationViewModel
}
}
private List<ApplicationViewModel> convertPackagesToViewModels(List<PackageInfo> infos,
private void convertPackagesToViewModels(List<ApplicationViewModel> appsToBuild,
DatabaseManager databaseManager,
PackageManager packageManager) {
ArrayList<ApplicationViewModel> appsToBuild = new ArrayList<>(infos.size());
for (PackageInfo pi : infos) {
appsToBuild.add(buildViewModelFromPackageInfo(pi, databaseManager, packageManager));
for (ApplicationViewModel vm : appsToBuild) {
try {
PackageInfo pi = packageManager.getPackageInfo(vm.packageName, PackageManager.GET_PERMISSIONS);
buildViewModelFromPackageInfo(vm, pi, databaseManager, packageManager);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
return appsToBuild;
}
private ApplicationViewModel buildViewModelFromPackageInfo(PackageInfo pi,
private void buildViewModelFromPackageInfo(ApplicationViewModel vm, PackageInfo pi,
DatabaseManager databaseManager,
PackageManager packageManager) {
ApplicationViewModel vm = new ApplicationViewModel();
vm.versionName = pi.versionName;
vm.packageName = pi.packageName;
@ -76,9 +80,9 @@ class ComputeAppListTask extends AsyncTask<Void, Void, List<ApplicationViewModel
vm.requestedPermissions = pi.requestedPermissions;
if (vm.versionName != null)
vm.report = databaseManager.getReportFor(vm.packageName, vm.versionName);
vm.report = databaseManager.getReportFor(vm.packageName, vm.versionName, vm.source);
else {
vm.report = databaseManager.getReportFor(vm.packageName, vm.versionCode);
vm.report = databaseManager.getReportFor(vm.packageName, vm.versionCode, vm.source);
}
if (vm.report != null) {
@ -94,35 +98,41 @@ class ComputeAppListTask extends AsyncTask<Void, Void, List<ApplicationViewModel
vm.label = packageManager.getApplicationLabel(pi.applicationInfo);
vm.installerPackageName = packageManager.getInstallerPackageName(vm.packageName);
vm.isVisible = true;
return vm;
}
private void applyStoreFilter(List<PackageInfo> packageInfos,
private List<ApplicationViewModel> applyStoreFilter(List<PackageInfo> packageInfos,
DatabaseManager databaseManager,
PackageManager packageManager) {
List<PackageInfo> toRemove = new ArrayList<>();
List<ApplicationViewModel> result = new ArrayList<>();
for (PackageInfo packageInfo : packageInfos) {
String packageName = packageInfo.packageName;
String installerPackageName = packageManager.getInstallerPackageName(packageName);
if (!gStore.equals(installerPackageName)) {
ApplicationViewModel vm = new ApplicationViewModel();
vm.packageName = packageName;
if (!gStore.equals(installerPackageName) && !fdroid.equals(installerPackageName)) {
String auid = Utils.getCertificateSHA1Fingerprint(packageManager, packageName);
String appuid = databaseManager.getAUID(packageName);
if(!auid.equalsIgnoreCase(appuid)) {
toRemove.add(packageInfo);
Map<String,String> sources = databaseManager.getSources(packageName);
for(Map.Entry<String,String> entry : sources.entrySet()) {
if(entry.getValue().equalsIgnoreCase(auid)) {
vm.source = entry.getKey();
break;
}
}
} else if (gStore.equals(installerPackageName)) {
vm.source = "google";
} else {
vm.source = "fdroid";
}
ApplicationInfo appInfo = null;
try {
ApplicationInfo appInfo = packageManager.getApplicationInfo(packageName,0);
if(!appInfo.enabled) {
toRemove.add(packageInfo);
}
appInfo = packageManager.getApplicationInfo(packageName,0);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
if(vm.source != null && appInfo != null && appInfo.enabled)
result.add(vm);
}
packageInfos.removeAll(toRemove);
return result;
}
}

View File

@ -29,8 +29,10 @@ import org.eu.exodus_privacy.exodusprivacy.objects.Report;
import org.eu.exodus_privacy.exodusprivacy.objects.Tracker;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class DatabaseManager extends SQLiteOpenHelper {
@ -44,14 +46,14 @@ public class DatabaseManager extends SQLiteOpenHelper {
public static DatabaseManager getInstance(Context context) {
if(instance == null)
instance = new DatabaseManager(context,"Exodus.db",null,2);
instance = new DatabaseManager(context,"Exodus.db",null,3);
return instance;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("Create Table if not exists applications (id INTEGER primary key autoincrement, package TEXT, name TEXT, creator TEXT, auid TEXT);");
db.execSQL("Create Table if not exists reports (id INTEGER primary key, creation INTEGER, updateat INTEGER, downloads TEXT, version TEXT, version_code INTEGER, app_id INTEGER, foreign key(app_id) references applications(id));");
db.execSQL("Create Table if not exists applications (id INTEGER primary key autoincrement, package TEXT, name TEXT, creator TEXT, sources TEXT);");
db.execSQL("Create Table if not exists reports (id INTEGER primary key, creation INTEGER, updateat INTEGER, downloads TEXT, version TEXT, version_code INTEGER, app_id INTEGER, source TEXT, foreign key(app_id) references applications(id));");
db.execSQL("Create Table if not exists trackers (id INTEGER primary key, name TEXT, creation_date INTEGER, code_signature TEXT, network_signature TEXT, website TEXT, description TEXT);");
db.execSQL("Create Table if not exists trackers_reports (id INTEGER primary key autoincrement, tracker_id INTEGER, report_id INTEGER, foreign key(tracker_id) references trackers(id), foreign key(report_id) references reports(id));");
@ -59,10 +61,36 @@ public class DatabaseManager extends SQLiteOpenHelper {
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// do nothing
if(newVersion >= 2) {
if(oldVersion <= 1) {
db.execSQL("Alter Table applications add column auid TEXT");
}
if (oldVersion <= 2) {
try {
db.beginTransaction();
db.execSQL("Alter Table reports add column source TEXT");
db.execSQL("Alter Table applications rename to old_apps");
db.execSQL("Create Table if not exists applications (id INTEGER primary key autoincrement, package TEXT, name TEXT, creator TEXT, sources TEXT);");
Cursor cursor = db.query("old_apps",null,null,null,null,null,null);
while (cursor.moveToNext()){
ContentValues values = new ContentValues();
values.put("package",cursor.getString(1));
values.put("name",cursor.getString(2));
values.put("creator",cursor.getString(3));
String sources = "unknown:"+cursor.getString(4)+"|";
values.put("sources",sources);
db.insert("applications",null,values);
}
cursor.close();
db.execSQL("Drop Table old_apps");
db.setTransactionSuccessful();
} catch (Exception e) {
e.printStackTrace();
} finally {
db.endTransaction();
}
}
}
private boolean existReport(SQLiteDatabase db, long reportId) {
@ -122,7 +150,7 @@ public class DatabaseManager extends SQLiteOpenHelper {
values.put("package", application.packageName);
values.put("name",application.name);
values.put("creator",application.creator);
values.put("auid",application.auid);
values.put("sources",buildSourcesStr(application.sources));
if(!existApplication(db, application.packageName)) {
db.insert("applications", null, values);
@ -155,6 +183,7 @@ public class DatabaseManager extends SQLiteOpenHelper {
values.put("version",report.version);
values.put("version_code",report.versionCode);
values.put("app_id",appId);
values.put("source",report.source);
if(!existReport(db,report.id)) {
values.put("id",report.id);
@ -184,7 +213,7 @@ public class DatabaseManager extends SQLiteOpenHelper {
db.insert("trackers_reports",null,values);
}
public Report getReportFor(String packageName, String version) {
public Report getReportFor(String packageName, String version, String source) {
SQLiteDatabase db = getReadableDatabase();
String[] columns = {"id"};
String where = "package = ?";
@ -193,10 +222,11 @@ public class DatabaseManager extends SQLiteOpenHelper {
if(cursor.moveToFirst()) {
long appId = cursor.getLong(0);
cursor.close();
where = "app_id = ? and version = ?";
whereArgs = new String[2];
where = "app_id = ? and version = ? and source = ?";
whereArgs = new String[3];
whereArgs[0] = String.valueOf(appId);
whereArgs[1] = version;
whereArgs[2] = source;
String order = "id ASC";
cursor = db.query("reports",columns,where,whereArgs,null,null,order);
long reportId;
@ -208,9 +238,10 @@ public class DatabaseManager extends SQLiteOpenHelper {
columns = new String[2];
columns[0] = "id";
columns[1] = "creation";
where = "app_id = ?";
whereArgs = new String[1];
where = "app_id = ? and source = ?";
whereArgs = new String[2];
whereArgs[0] = String.valueOf(appId);
whereArgs[1] = source;
order = "creation DESC";
//search a recent reports
cursor = db.query("reports",columns,where,whereArgs,null,null,order);
@ -230,7 +261,7 @@ public class DatabaseManager extends SQLiteOpenHelper {
}
}
public Report getReportFor(String packageName, long version) {
public Report getReportFor(String packageName, long version, String source) {
SQLiteDatabase db = getReadableDatabase();
String[] columns = {"id"};
String where = "package = ?";
@ -239,10 +270,11 @@ public class DatabaseManager extends SQLiteOpenHelper {
if(cursor.moveToFirst()) {
long appId = cursor.getLong(0);
cursor.close();
where = "app_id = ? and version_code = ?";
whereArgs = new String[2];
where = "app_id = ? and version_code = ? and source = ?";
whereArgs = new String[3];
whereArgs[0] = String.valueOf(appId);
whereArgs[1] = String.valueOf(version);
whereArgs[2] = source;
String order = "id ASC";
cursor = db.query("reports",columns,where,whereArgs,null,null,order);
long reportId;
@ -254,9 +286,10 @@ public class DatabaseManager extends SQLiteOpenHelper {
columns = new String[2];
columns[0] = "id";
columns[1] = "creation";
where = "app_id = ?";
whereArgs = new String[1];
where = "app_id = ? and source = ?";
whereArgs = new String[2];
whereArgs[0] = String.valueOf(appId);
whereArgs[1] = source;
order = "creation DESC";
//search a recent reports
cursor = db.query("reports",columns,where,whereArgs,null,null,order);
@ -301,7 +334,8 @@ public class DatabaseManager extends SQLiteOpenHelper {
report.downloads = cursor.getString(col++);
report.version = cursor.getString(col++);
report.versionCode = cursor.getLong(col++);
report.appId = cursor.getLong(col);
report.appId = cursor.getLong(col++);
report.source = cursor.getString(col);
cursor.close();
report.trackers = new HashSet<>();
@ -372,17 +406,39 @@ public class DatabaseManager extends SQLiteOpenHelper {
}
}
public String getAUID(String packageName) {
public Map<String,String> getSources(String packageName) {
String where = "package = ?";
String[] whereArgs = {packageName};
String[] columns = {"auid"};
String[] columns = {"sources"};
Cursor cursor = getReadableDatabase().query("applications",columns,where,whereArgs,null,null,null,null);
String uaid="";
String sourcesStr="";
if(cursor.moveToFirst())
{
uaid = cursor.getString(0);
sourcesStr = cursor.getString(0);
}
cursor.close();
return uaid;
return extractSources(sourcesStr);
}
private String buildSourcesStr(Map<String,String> sources) {
StringBuilder sourceStr = new StringBuilder();
for(Map.Entry<String,String> entry : sources.entrySet()) {
sourceStr.append(entry.getKey()).append(":").append(entry.getValue()).append("|");
}
return sourceStr.toString();
}
private Map<String, String> extractSources(String sourcesStr) {
Map<String,String> sources = new HashMap<>();
String[] sourceList = sourcesStr.split("\\|");
for(String sourceItem : sourceList){
if(!sourceItem.isEmpty()) {
System.out.println(sourceItem);
String[] data = sourceItem.split(":");
sources.put(data[0], data[1]);
}
}
return sources;
}
}

View File

@ -231,7 +231,7 @@ public class NetworkManager {
if(object != null) {
Map<String,String> handles = new HashMap<>();
Map<String,Map<String,String>> handles = new HashMap<>();
ArrayList<String> packages = mes.args.getStringArrayList("packages");
if (packages == null)
return;
@ -244,9 +244,14 @@ public class NetworkManager {
JSONObject app = applications.getJSONObject(i);
String handle = app.getString("handle");
String auid = app.getString("app_uid");
String source = app.getString("source");
Map<String,String> sources = handles.get(handle);
if(sources == null)
sources = new HashMap<>();
sources.put(source,auid);
if (packages.contains(handle))
handles.put(handle,auid);
app.remove("app_uid");
handles.put(handle,sources);
}
//remove app not analyzed by Exodus
@ -259,6 +264,7 @@ public class NetworkManager {
int val = rand.nextInt(applications.length());
JSONObject app = applications.getJSONObject(val);
String handle = app.getString("handle");
handles.put(handle,new HashMap<>());
packages.add(handle);
}
//shuffle the list
@ -275,14 +281,14 @@ public class NetworkManager {
mes.listener.onSuccess();
}
private void getReports(Message mes, Map<String, String> handles, ArrayList<String> packages) {
private void getReports(Message mes, Map<String, Map<String,String>> handles, ArrayList<String> packages) {
for(int i = 0; i < packages.size(); i++) {
mes.listener.onProgress(R.string.parse_application,i+1,packages.size());
getReport(mes,packages.get(i),handles.get(packages.get(i)));
}
}
private void getReport(Message mes, String handle, String auid) {
private void getReport(Message mes, String handle, Map<String,String> sources) {
URL url;
try {
url = new URL(apiUrl+"search/"+handle);
@ -298,7 +304,7 @@ public class NetworkManager {
ArrayList<String> packages = mes.args.getStringArrayList("packages");
if(packages != null && packages.contains(handle)) {
Application app = parseApplication(application, handle);
app.auid = auid;
app.sources = sources;
DatabaseManager.getInstance(mes.context).insertOrUpdateApplication(app);
}
} catch (JSONException e) {
@ -328,6 +334,7 @@ public class NetworkManager {
report.id = object.getLong("id");
report.downloads = object.getString("downloads");
report.version = object.getString("version");
report.source = object.getString("source");
if(!object.getString("version_code").isEmpty())
report.versionCode = Long.parseLong(object.getString("version_code"));
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());

View File

@ -18,6 +18,8 @@
package org.eu.exodus_privacy.exodusprivacy.objects;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public class Application {
@ -26,7 +28,7 @@ public class Application {
public String name;
public String creator;
public Set<Report> reports;
public String auid;
public Map<String,String> sources;
@Override
public boolean equals(Object o) {
@ -36,14 +38,13 @@ public class Application {
Application that = (Application) o;
if (id != that.id) return false;
if (!auid.equals(that.auid)) return false;
return packageName.equals(that.packageName);
return packageName != null ? packageName.equals(that.packageName) : that.packageName == null;
}
@Override
public int hashCode() {
int result = (int) (id ^ (id >>> 32));
result = 31 * result + packageName.hashCode();
result = 31 * result + (packageName != null ? packageName.hashCode() : 0);
return result;
}
}

View File

@ -30,6 +30,7 @@ public class Report {
public long versionCode;
public Set<Long> trackers;
public long appId;
public String source;
@Override
public boolean equals(Object o) {

View File

@ -9,6 +9,7 @@ import android.content.pm.PermissionInfo;
import android.graphics.drawable.Drawable;
import android.os.Build;
import org.eu.exodus_privacy.exodusprivacy.R;
import org.eu.exodus_privacy.exodusprivacy.adapters.ApplicationViewModel;
import org.eu.exodus_privacy.exodusprivacy.manager.DatabaseManager;
@ -26,6 +27,7 @@ public class ReportDisplay {
public Drawable logo;
public List<Permission> permissions;
public Set<Tracker> trackers;
public String source;
private ReportDisplay(){
@ -40,6 +42,7 @@ public class ReportDisplay {
reportDisplay.displayName = model.label.toString();
reportDisplay.report = model.report;
reportDisplay.source = context.getString(R.string.source,model.source);
reportDisplay.trackers = model.trackers;

View File

@ -83,6 +83,12 @@
</RelativeLayout>
<TextView
android:layout_below="@+id/base_info"
android:id="@+id/source"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
android:layout_below="@+id/source"
android:id="@+id/other_version"
android:text="@string/tested"
android:layout_width="match_parent"

View File

@ -168,6 +168,20 @@
android:textStyle="bold"
android:textColor="@color/textColorDark"/>
<TextView
android:id="@+id/source"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{reportInfo.source}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/report_version_value"
android:layout_marginTop="5dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:textSize="16sp"
android:textColor="@color/textColorDark"/>
<TextView
android:id="@+id/creator"
android:layout_width="0dp"
@ -175,7 +189,7 @@
android:text="@string/created_by"
android:visibility="@{reportInfo.creatorVisibility ? View.VISIBLE : View.GONE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/report_version_value"
app:layout_constraintTop_toBottomOf="@id/source"
android:layout_marginTop="5dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
@ -189,7 +203,7 @@
android:visibility="@{reportInfo.creatorVisibility ? View.VISIBLE : View.GONE}"
app:layout_constraintStart_toEndOf="@id/creator"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/report_version_value"
app:layout_constraintTop_toBottomOf="@id/source"
android:layout_marginTop="5dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="20dp"

View File

@ -15,7 +15,7 @@
<string name="tested">Il n\'y a pas de rapport pour la version installée (%1$s), les informations affichées sont basées sur une autre version (%2$s)</string>
<string name="analysed">Cette application n\'a pas encore été analysée par Exodus Privacy.</string>
<string name="no_package_manager">Votre système semble ne pas donner accès aux applications installées.</string>
<string name="no_app_found">Vous semblez n\'avoir aucune application installée par la source que nous recherchons (Google Play).</string>
<string name="no_app_found">Vous semblez n\'avoir aucune application installée par la source que nous recherchons (Google Play ou F-Droid).</string>
<string name="get_reports_connection">Récupération des applications : en attente de connexion au serveur</string>
<string name="get_reports">Récupération des applications</string>
<string name="parse_application">Traitement des applications :</string>
@ -44,6 +44,7 @@
<string name="tracker_presence">Présent dans %d de vos applications</string>
<string name="tracker_presence_in">Présent dans:</string>
<string name="no_app_found_tracker">Ce pisteur ne semble pas être présent dans vos applications</string>
<string name="source">Source: %s</string>
<!-- Menu -->
<string name="menu_action_filter">Filtrer</string>

View File

@ -15,7 +15,7 @@
<string name="tested">There\'s no report for the installed version (%1$s), the information displayed is based on another (%2$s) version</string>
<string name="analysed">This app hasn\'t been analysed by Exodus Privacy yet.</string>
<string name="no_package_manager">It appears that your system doesn\'t allow access to the list of installed apps.</string>
<string name="no_app_found">It appears that you don\'t have any apps installed from the source we test (Google Play store).</string>
<string name="no_app_found">It appears that you don\'t have any apps installed from the source we test (Google Play store or F-Droid).</string>
<string name="get_reports_connection">Getting applications: Waiting for server connection</string>
<string name="get_reports">Getting applications</string>
<string name="parse_application">Processing Applications:</string>
@ -44,6 +44,7 @@
<string name="tracker_presence">Present in %d of your applications</string>
<string name="tracker_presence_in">Present in:</string>
<string name="no_app_found_tracker">This tracker seems not be present in your applications</string>
<string name="source">Source: %s</string>
<!-- Menu -->
<string name="menu_action_filter">Filter</string>