diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/OfflineProduct.java b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/OfflineProduct.java new file mode 100644 index 000000000000..b827e56aed1c --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/OfflineProduct.java @@ -0,0 +1,98 @@ +package openfoodfacts.github.scrachx.openfood.models; + +import com.opencsv.bean.CsvBindByName; + +import org.greenrobot.greendao.annotation.Entity; +import org.greenrobot.greendao.annotation.Generated; +import org.greenrobot.greendao.annotation.Id; +import org.greenrobot.greendao.annotation.Index; + +@Entity(indexes = { + @Index(value = "barcode", unique = true) +}) +public class OfflineProduct { + + @Id + private Long id; + @CsvBindByName(column = "product_name") + private String title; + @CsvBindByName(column = "brands") + private String brands; + @CsvBindByName(column = "code") + private String barcode; + @CsvBindByName(column = "quantity") + private String quantity; + @CsvBindByName(column = "nutrition_grade_fr") + private String nutritionGrade; + + public OfflineProduct(String title, String brands, String barcode, String quantity, String nutritionGrade) { + this.title = title; + this.brands = brands; + this.barcode = barcode; + this.quantity = quantity; + this.nutritionGrade = nutritionGrade; + } + + @Generated(hash = 524339296) + public OfflineProduct(Long id, String title, String brands, String barcode, String quantity, + String nutritionGrade) { + this.id = id; + this.title = title; + this.brands = brands; + this.barcode = barcode; + this.quantity = quantity; + this.nutritionGrade = nutritionGrade; + } + + @Generated(hash = 1425505421) + public OfflineProduct() { + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getBrands() { + return brands; + } + + public void setBrands(String brands) { + this.brands = brands; + } + + public String getBarcode() { + return barcode; + } + + public void setBarcode(String barcode) { + this.barcode = barcode; + } + + public Long getId() { + return this.id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getQuantity() { + return quantity; + } + + public void setQuantity(String quantity) { + this.quantity = quantity; + } + + public String getNutritionGrade() { + return nutritionGrade; + } + + public void setNutritionGrade(String nutritionGrade) { + this.nutritionGrade = nutritionGrade; + } +} diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/OpenFoodAPIService.java b/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/OpenFoodAPIService.java index 05231acb9272..701968a66f18 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/OpenFoodAPIService.java +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/OpenFoodAPIService.java @@ -375,4 +375,13 @@ Single getIngredients(@Query("code") String code, Call editImages(@Query("code") String code, @QueryMap Map fields); + + /** + * This method downloads the file with a dynamic downloadable url + * + * @param fileUrl + * @return + */ + @GET + Call downloadFileWithDynamicUrlSync(@Url String fileUrl); } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Utils.java b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Utils.java index 10d1d0784443..a35480a23249 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Utils.java +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Utils.java @@ -81,6 +81,7 @@ public class Utils { public static final int MY_PERMISSIONS_REQUEST_CAMERA = 1; public static final int MY_PERMISSIONS_REQUEST_STORAGE = 2; public static final String UPLOAD_JOB_TAG = "upload_saved_product_job"; + private static final String TAG = "Utils"; public static boolean isUploadJobInitialised; public static boolean DISABLE_IMAGE_LOAD = false; @@ -558,6 +559,24 @@ public static boolean isExternalStorageWritable() { return Environment.MEDIA_MOUNTED.equals(state); } + public static boolean isStoragePermissionGranted(Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (activity.checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED) { + Log.v(TAG, "Permission is granted"); + return true; + } else { + + Log.v(TAG, "Permission is revoked"); + ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); + return false; + } + } else { //permission is automatically granted on sdk<23 upon installation + Log.v(TAG, "Permission is granted"); + return true; + } + } + public static Uri getOutputPicUri(Context context) { return (Uri.fromFile(new File(Utils.makeOrGetPictureDirectory(context), "/" + Utils.timeStamp() + ".jpg"))); } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/views/splash/SplashActivity.java b/app/src/main/java/openfoodfacts/github/scrachx/openfood/views/splash/SplashActivity.java index 383306bdd2cd..3214c1558db2 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/views/splash/SplashActivity.java +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/views/splash/SplashActivity.java @@ -1,28 +1,64 @@ package openfoodfacts.github.scrachx.openfood.views.splash; +import android.annotation.SuppressLint; +import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; import android.content.res.AssetManager; +import android.os.AsyncTask; import android.os.Bundle; +import android.os.Environment; +import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; +import android.util.Log; import android.widget.TextView; +import com.afollestad.materialdialogs.DialogAction; +import com.afollestad.materialdialogs.MaterialDialog; +import com.opencsv.CSVReader; + import net.steamcrafted.loadtoast.LoadToast; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.util.Iterator; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + import butterknife.BindView; +import okhttp3.ResponseBody; import openfoodfacts.github.scrachx.openfood.R; +import openfoodfacts.github.scrachx.openfood.models.OfflineProduct; +import openfoodfacts.github.scrachx.openfood.models.OfflineProductDao; +import openfoodfacts.github.scrachx.openfood.network.OpenFoodAPIClient; +import openfoodfacts.github.scrachx.openfood.utils.Utils; import openfoodfacts.github.scrachx.openfood.views.BaseActivity; import openfoodfacts.github.scrachx.openfood.views.WelcomeActivity; import pl.aprilapps.easyphotopicker.EasyImage; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; public class SplashActivity extends BaseActivity implements ISplashPresenter.View { + private static final String TAG = "SplashActivity"; + private static final int BUFFER_SIZE = 4096; @BindView(R.id.tagline) TextView tagline; + @BindView(R.id.text_loading) + TextView textLoading; int i = 0; private ISplashPresenter.Actions presenter; private LoadToast toast; private String[] taglines; + SharedPreferences settings; /* To show different slogans below the logo while content is being downloaded. @@ -51,7 +87,17 @@ public void onCreate(Bundle savedInstanceState) { toast = new LoadToast(this); presenter = new SplashPresenter(getSharedPreferences("prefs", 0), this); - presenter.refreshData(); + settings = getSharedPreferences("prefs", Context.MODE_PRIVATE); + if (!settings.getBoolean("is_offline_data_available", false)) { + //Dialog to ask for download + new MaterialDialog.Builder(this) + .content(R.string.download_offline_product) + .positiveText(R.string.txtDownload) + .negativeText(R.string.txtPictureNeededDialogNo) + .onPositive((dialog, which) -> doDownload()) + .onNegative((dialog, which) -> presenter.refreshData()) + .show(); + } } @Override @@ -85,4 +131,230 @@ public void hideLoading(boolean isError) { public AssetManager getAssetManager() { return getAssets(); } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Log.v(TAG, "Permission: " + permissions[0] + "was " + grantResults[0]); + //resume tasks needing this permission + downloadOfflineProducts(); + } + } + + private void doDownload() { + if (Utils.isStoragePermissionGranted(this)) { + downloadOfflineProducts(); + } + } + + /** + * This method download the csv file and save it to the database. + */ + private void downloadOfflineProducts() { + if (Utils.isNetworkConnected(this) && !Utils.isConnectedToMobileData(this)) { + textLoading.setText("Downloading Data..."); + OpenFoodAPIClient client = new OpenFoodAPIClient(this); + String fileURL = "http://fr.openfoodfacts.org/data/offline/fr.openfoodfacts.org.products.small.zip"; + client.getAPIService().downloadFileWithDynamicUrlSync(fileURL) + .enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + textLoading.setText(getString(R.string.txtProcessing)); + Log.d(TAG, "server contacted and has file"); + unzip(response.body(), Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS + File.separator + "openfoodfacts").getAbsolutePath()); + } else { + new MaterialDialog.Builder(SplashActivity.this) + .content("Connection failed!") + .positiveText("Retry") + .negativeText(R.string.txtPictureNeededDialogNo) + .onPositive((dialog, which) -> doDownload()) + .onNegative(new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + textLoading.setText(getString(R.string.txtLoading)); + presenter.refreshData(); + } + }) + .show(); + Log.d(TAG, "server contact failed"); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + new MaterialDialog.Builder(SplashActivity.this) + .content("Connection failed!") + .positiveText("Retry") + .negativeText(R.string.txtPictureNeededDialogNo) + .onPositive((dialog, which) -> doDownload()) + .onNegative(new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + textLoading.setText(getString(R.string.txtLoading)); + presenter.refreshData(); + } + }) + .show(); + Log.e(TAG, "error"); + } + }); + } else { + new MaterialDialog.Builder(SplashActivity.this) + .content("Wifi Connection not found!") + .positiveText("Retry") + .negativeText(R.string.txtPictureNeededDialogNo) + .onPositive((dialog, which) -> doDownload()) + .onNegative(new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + textLoading.setText(getString(R.string.txtLoading)); + presenter.refreshData(); + } + }) + .show(); + } + } + + public void unzip(ResponseBody responseBody, String _location) { + try { + ZipInputStream zin = new ZipInputStream(responseBody.byteStream()); + + byte b[] = new byte[1024]; + String string = ""; + + ZipEntry ze = null; + while ((ze = zin.getNextEntry()) != null) { + Log.v("Decompress", "Unzipping " + ze.getName()); + + if (ze.isDirectory()) { + _dirChecker(_location, ze.getName()); + } else { + + if (!new File(_location).isDirectory()) { + new File(_location).mkdirs(); + } + FileOutputStream fout = new FileOutputStream(_location + File.separator + ze.getName()); + string = ze.getName(); + BufferedInputStream in = new BufferedInputStream(zin); + BufferedOutputStream out = new BufferedOutputStream(fout); + + int n; + while ((n = in.read(b, 0, 1024)) >= 0) { + out.write(b, 0, n); + } + + zin.closeEntry(); + out.close(); + } + + } + new saveCSVToDb(new File(_location + File.separator + string), settings, this).execute(); + zin.close(); + } catch (Exception e) { + Log.e("Decompress", "unzip", e); + new MaterialDialog.Builder(SplashActivity.this) + .content(R.string.txtConnectionFailed) + .positiveText(getString(R.string.retry)) + .negativeText(R.string.txtPictureNeededDialogNo) + .onPositive((dialog, which) -> doDownload()) + .onNegative(new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + textLoading.setText(getString(R.string.txtLoading)); + presenter.refreshData(); + } + }) + .show(); + } + + } + + private void _dirChecker(String _location, String dir) { + File f = new File(_location + dir); + + if (!f.isDirectory()) { + f.mkdirs(); + } + } + + private class saveCSVToDb extends AsyncTask { + File file; + @SuppressLint("StaticFieldLeak") + Context context; + SharedPreferences settings; + + private saveCSVToDb(File file, SharedPreferences settings, Context context) { + this.file = file; + this.settings = settings; + this.context = context; + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + } + + @Override + protected void onPostExecute(Boolean is) { + super.onPostExecute(is); + if (is) { + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean("is_offline_data_available", true); + editor.apply(); + textLoading.setText(getString(R.string.txtLoading)); + presenter.refreshData(); + } else { + new MaterialDialog.Builder(SplashActivity.this) + .content(getString(R.string.txtConnectionFailed)) + .positiveText(getString(R.string.retry)) + .negativeText(R.string.txtPictureNeededDialogNo) + .onPositive((dialog, which) -> doDownload()) + .onNegative(new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + textLoading.setText(getString(R.string.txtLoading)); + presenter.refreshData(); + } + }) + .show(); + } + } + + @Override + protected Boolean doInBackground(Void... voids) { + OfflineProductDao mOfflineProductDao = Utils.getAppDaoSession(context).getOfflineProductDao(); + //parsing csv file + try (CSVReader reader = new CSVReader(new FileReader(file.getAbsolutePath()), '\t')) { + List records = reader.readAll(); + Iterator iterator = records.iterator(); + // To skip header in the csv file + iterator.hasNext(); + long size = records.size(); + long count = 0; + while (iterator.hasNext()) { + String[] record = iterator.next(); + OfflineProduct offlineProduct = new OfflineProduct(record[1], record[3], record[0], record[2], record[4]); + //saving to db + mOfflineProductDao.insertOrReplace(offlineProduct); + count++; + Log.d(TAG, "completed " + count + " / " + size); + long finalCount = count; + runOnUiThread(new Runnable() { + @Override + public void run() { + textLoading.setText(getString(R.string.txtSaving) + ((finalCount * 100) / size) + "%)"); + } + }); + Log.d(TAG, "progress " + ((float) count / (float) size) * 100 + "% "); + } + } catch (IOException e) { + e.printStackTrace(); + return false; + } + return true; + } + } + } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aee7a949b0c1..5fdea5a137bc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -941,7 +941,7 @@ Enables fast addition of products with fewer attributes Search for a additive Additives - + Add to your lists Create a new list @@ -964,5 +964,12 @@ Select your country Change your selected country Country + + Do you want to download data to search product offline? + Download + Processing Data... + Saving to database (Progress + Connection failed! + Retry