Schedule update remove

This commit is contained in:
stom79 2019-01-20 15:20:07 +01:00
parent 284c118daf
commit f048eea4e1
9 changed files with 459 additions and 129 deletions

View File

@ -102,12 +102,14 @@ import java.util.regex.Pattern;
import es.dmoral.toasty.Toasty; import es.dmoral.toasty.Toasty;
import fr.gouv.etalab.mastodon.R; import fr.gouv.etalab.mastodon.R;
import fr.gouv.etalab.mastodon.asynctasks.PostActionAsyncTask;
import fr.gouv.etalab.mastodon.asynctasks.PostStatusAsyncTask; import fr.gouv.etalab.mastodon.asynctasks.PostStatusAsyncTask;
import fr.gouv.etalab.mastodon.asynctasks.RetrieveAccountsForReplyAsyncTask; import fr.gouv.etalab.mastodon.asynctasks.RetrieveAccountsForReplyAsyncTask;
import fr.gouv.etalab.mastodon.asynctasks.RetrieveEmojiAsyncTask; import fr.gouv.etalab.mastodon.asynctasks.RetrieveEmojiAsyncTask;
import fr.gouv.etalab.mastodon.asynctasks.RetrieveSearchAccountsAsyncTask; import fr.gouv.etalab.mastodon.asynctasks.RetrieveSearchAccountsAsyncTask;
import fr.gouv.etalab.mastodon.asynctasks.RetrieveSearchAsyncTask; import fr.gouv.etalab.mastodon.asynctasks.RetrieveSearchAsyncTask;
import fr.gouv.etalab.mastodon.asynctasks.UpdateDescriptionAttachmentAsyncTask; import fr.gouv.etalab.mastodon.asynctasks.UpdateDescriptionAttachmentAsyncTask;
import fr.gouv.etalab.mastodon.client.API;
import fr.gouv.etalab.mastodon.client.APIResponse; import fr.gouv.etalab.mastodon.client.APIResponse;
import fr.gouv.etalab.mastodon.client.Entities.Account; import fr.gouv.etalab.mastodon.client.Entities.Account;
import fr.gouv.etalab.mastodon.client.Entities.Attachment; import fr.gouv.etalab.mastodon.client.Entities.Attachment;
@ -129,6 +131,7 @@ import fr.gouv.etalab.mastodon.drawers.TagsSearchAdapter;
import fr.gouv.etalab.mastodon.helper.Helper; import fr.gouv.etalab.mastodon.helper.Helper;
import fr.gouv.etalab.mastodon.helper.MastalabAutoCompleteTextView; import fr.gouv.etalab.mastodon.helper.MastalabAutoCompleteTextView;
import fr.gouv.etalab.mastodon.interfaces.OnDownloadInterface; import fr.gouv.etalab.mastodon.interfaces.OnDownloadInterface;
import fr.gouv.etalab.mastodon.interfaces.OnPostActionInterface;
import fr.gouv.etalab.mastodon.interfaces.OnPostStatusActionInterface; import fr.gouv.etalab.mastodon.interfaces.OnPostStatusActionInterface;
import fr.gouv.etalab.mastodon.interfaces.OnRetrieveAccountsReplyInterface; import fr.gouv.etalab.mastodon.interfaces.OnRetrieveAccountsReplyInterface;
import fr.gouv.etalab.mastodon.interfaces.OnRetrieveAttachmentInterface; import fr.gouv.etalab.mastodon.interfaces.OnRetrieveAttachmentInterface;
@ -152,7 +155,7 @@ import static fr.gouv.etalab.mastodon.helper.Helper.convertDpToPixel;
* Toot activity class * Toot activity class
*/ */
public class TootActivity extends BaseActivity implements OnRetrieveSearcAccountshInterface, OnRetrieveAttachmentInterface, OnPostStatusActionInterface, OnRetrieveSearchInterface, OnRetrieveAccountsReplyInterface, OnRetrieveEmojiInterface, OnDownloadInterface { public class TootActivity extends BaseActivity implements OnPostActionInterface, OnRetrieveSearcAccountshInterface, OnRetrieveAttachmentInterface, OnPostStatusActionInterface, OnRetrieveSearchInterface, OnRetrieveAccountsReplyInterface, OnRetrieveEmojiInterface, OnDownloadInterface {
private String visibility; private String visibility;
@ -198,7 +201,8 @@ public class TootActivity extends BaseActivity implements OnRetrieveSearcAccount
private boolean restoredScheduled; private boolean restoredScheduled;
static boolean active = false; static boolean active = false;
private int style; private int style;
private StoredStatus scheduledstatus;
private boolean isScheduled;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -275,7 +279,7 @@ public class TootActivity extends BaseActivity implements OnRetrieveSearcAccount
toot_sensitive = findViewById(R.id.toot_sensitive); toot_sensitive = findViewById(R.id.toot_sensitive);
drawer_layout = findViewById(R.id.drawer_layout); drawer_layout = findViewById(R.id.drawer_layout);
ImageButton toot_emoji = findViewById(R.id.toot_emoji); ImageButton toot_emoji = findViewById(R.id.toot_emoji);
isScheduled = false;
if( sharedpreferences.getBoolean(Helper.SET_DISPLAY_EMOJI, true)) { if( sharedpreferences.getBoolean(Helper.SET_DISPLAY_EMOJI, true)) {
final EmojiPopup emojiPopup = EmojiPopup.Builder.fromRootView(drawer_layout).build(toot_content); final EmojiPopup emojiPopup = EmojiPopup.Builder.fromRootView(drawer_layout).build(toot_content);
@ -315,6 +319,7 @@ public class TootActivity extends BaseActivity implements OnRetrieveSearcAccount
restored = -1; restored = -1;
if(b != null) { if(b != null) {
tootReply = b.getParcelable("tootReply"); tootReply = b.getParcelable("tootReply");
scheduledstatus = b.getParcelable("storedStatus");
String accountReplyToken = b.getString("accountReplyToken", null); String accountReplyToken = b.getString("accountReplyToken", null);
accountReply = null; accountReply = null;
if( accountReplyToken != null){ if( accountReplyToken != null){
@ -348,7 +353,8 @@ public class TootActivity extends BaseActivity implements OnRetrieveSearcAccount
} }
restored = b.getLong("restored", -1); restored = b.getLong("restored", -1);
} }
if( scheduledstatus != null)
toot_it.setText(R.string.modify);
if(restoredScheduled){ if(restoredScheduled){
toot_it.setVisibility(View.GONE); toot_it.setVisibility(View.GONE);
invalidateOptionsMenu(); invalidateOptionsMenu();
@ -627,6 +633,8 @@ public class TootActivity extends BaseActivity implements OnRetrieveSearcAccount
} }
}); });
if( scheduledstatus != null)
restoreServerSchedule(scheduledstatus.getStatus());
if( restored != -1 ){ if( restored != -1 ){
restoreToot(restored); restoreToot(restored);
@ -768,6 +776,16 @@ public class TootActivity extends BaseActivity implements OnRetrieveSearcAccount
} }
} }
@Override
public void onPostAction(int statusCode, API.StatusAction statusAction, String userId, Error error) {
if( error != null){
Toasty.error(getApplicationContext(),getString(R.string.toast_error),Toast.LENGTH_LONG).show();
}else {
Toasty.success(getApplicationContext(),getString(R.string.toot_scheduled),Toast.LENGTH_LONG).show();
resetForNextToot();
}
}
private class asyncPicture extends AsyncTask<Void, Void, Void> { private class asyncPicture extends AsyncTask<Void, Void, Void> {
ByteArrayInputStream bs; ByteArrayInputStream bs;
@ -1273,7 +1291,7 @@ public class TootActivity extends BaseActivity implements OnRetrieveSearcAccount
private void sendToot(String timestamp){ private void sendToot(String timestamp){
toot_it.setEnabled(false); toot_it.setEnabled(false);
if(toot_content.getText().toString().trim().length() == 0 && attachments.size() == 0){ if(toot_content.getText().toString().trim().length() == 0 && attachments.size() == 0){
Toasty.error(getApplicationContext(),getString(R.string.toot_select_image_error),Toast.LENGTH_LONG).show(); Toasty.error(getApplicationContext(),getString(R.string.toot_error_no_content),Toast.LENGTH_LONG).show();
toot_it.setEnabled(true); toot_it.setEnabled(true);
return; return;
} }
@ -1301,7 +1319,15 @@ public class TootActivity extends BaseActivity implements OnRetrieveSearcAccount
toot.setContent(tootContent); toot.setContent(tootContent);
if( timestamp == null) if( timestamp == null)
new PostStatusAsyncTask(getApplicationContext(), accountReply, toot, TootActivity.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); if( scheduledstatus == null)
new PostStatusAsyncTask(getApplicationContext(), accountReply, toot, TootActivity.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
else {
toot.setScheduled_at(Helper.dateToString(scheduledstatus.getScheduled_date()));
scheduledstatus.setStatus(toot);
isScheduled = true;
new PostActionAsyncTask(getApplicationContext(), API.StatusAction.DELETESCHEDULED, scheduledstatus, TootActivity.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
new PostStatusAsyncTask(getApplicationContext(), accountReply, toot, TootActivity.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
else { else {
toot.setScheduled_at(timestamp); toot.setScheduled_at(timestamp);
new PostStatusAsyncTask(getApplicationContext(), accountReply, toot, TootActivity.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); new PostStatusAsyncTask(getApplicationContext(), accountReply, toot, TootActivity.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
@ -1312,12 +1338,14 @@ public class TootActivity extends BaseActivity implements OnRetrieveSearcAccount
private void serverSchedule(String time){ private void serverSchedule(String time){
sendToot(time); sendToot(time);
isScheduled = true;
resetForNextToot(); resetForNextToot();
} }
private void deviceSchedule(long time){ private void deviceSchedule(long time){
//Store the toot as draft first //Store the toot as draft first
storeToot(false, false); storeToot(false, false);
isScheduled = true;
//Schedules the toot //Schedules the toot
ScheduledTootsSyncJob.schedule(getApplicationContext(), currentToId, time); ScheduledTootsSyncJob.schedule(getApplicationContext(), currentToId, time);
resetForNextToot(); resetForNextToot();
@ -1730,7 +1758,10 @@ public class TootActivity extends BaseActivity implements OnRetrieveSearcAccount
isSensitive = false; isSensitive = false;
toot_sensitive.setVisibility(View.GONE); toot_sensitive.setVisibility(View.GONE);
currentToId = -1; currentToId = -1;
Toasty.success(TootActivity.this, getString(R.string.toot_sent), Toast.LENGTH_LONG).show(); if( scheduledstatus == null && !isScheduled)
Toasty.success(TootActivity.this, getString(R.string.toot_sent), Toast.LENGTH_LONG).show();
else
Toasty.success(TootActivity.this, getString(R.string.toot_scheduled), Toast.LENGTH_LONG).show();
toot_it.setEnabled(true); toot_it.setEnabled(true);
//It's a reply, so the user will be redirect to its answer //It's a reply, so the user will be redirect to its answer
if( tootReply != null){ if( tootReply != null){
@ -2069,6 +2100,139 @@ public class TootActivity extends BaseActivity implements OnRetrieveSearcAccount
} }
private void restoreServerSchedule(Status status){
attachments = status.getMedia_attachments();
int childCount = toot_picture_container.getChildCount();
ArrayList<ImageView> toRemove = new ArrayList<>();
if( childCount > 0 ){
for(int i = 0 ; i < childCount ; i++){
if( toot_picture_container.getChildAt(i) instanceof ImageView)
toRemove.add((ImageView)toot_picture_container.getChildAt(i));
}
if( toRemove.size() > 0){
for(ImageView imageView: toRemove)
toot_picture_container.removeView(imageView);
}
toRemove.clear();
}
String content = status.getContent();
Pattern mentionLink = Pattern.compile("(<\\s?a\\s?href=\"https?:\\/\\/([\\da-z\\.-]+\\.[a-z\\.]{2,10})\\/(@[\\/\\w._-]*)\"\\s?[^.]*<\\s?\\/\\s?a\\s?>)");
Matcher matcher = mentionLink.matcher(content);
if (matcher.find()) {
content = matcher.replaceAll("$3@$2");
}
if( removed ) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
content = Html.fromHtml(content, Html.FROM_HTML_MODE_LEGACY).toString();
else
//noinspection deprecation
content = Html.fromHtml(content).toString();
}
if( attachments != null && attachments.size() > 0){
toot_picture_container.setVisibility(View.VISIBLE);
picture_scrollview.setVisibility(View.VISIBLE);
int i = 0 ;
for(final Attachment attachment: attachments){
String url = attachment.getPreview_url();
if( url == null || url.trim().equals(""))
url = attachment.getUrl();
final ImageView imageView = new ImageView(getApplicationContext());
imageView.setId(Integer.parseInt(attachment.getId()));
LinearLayout.LayoutParams imParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
imParams.setMargins(20, 5, 20, 5);
imParams.height = (int) Helper.convertDpToPixel(100, getApplicationContext());
imageView.setAdjustViewBounds(true);
imageView.setScaleType(ImageView.ScaleType.FIT_XY);
toot_picture_container.addView(imageView, i, imParams);
Glide.with(imageView.getContext())
.asBitmap()
.load(url)
.into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, Transition<? super Bitmap> transition) {
imageView.setImageBitmap(resource);
}
});
imageView.setTag(attachment.getId());
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
SharedPreferences sharedpreferences = getSharedPreferences(Helper.APP_PREFS, android.content.Context.MODE_PRIVATE);
String instanceVersion = sharedpreferences.getString(Helper.INSTANCE_VERSION + userId + instance, null);
if (instanceVersion != null) {
Version currentVersion = new Version(instanceVersion);
Version minVersion = new Version("2.0");
if (currentVersion.compareTo(minVersion) == 1 || currentVersion.equals(minVersion)) {
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
showAddDescription(attachment);
}
});
}
}
}
});
imageView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
showRemove(imageView.getId());
return false;
}
});
addBorder();
if( attachments.size() < 4)
toot_picture.setEnabled(true);
toot_sensitive.setVisibility(View.VISIBLE);
i++;
}
}else {
toot_picture_container.setVisibility(View.GONE);
}
//Sensitive content
toot_sensitive.setChecked(status.isSensitive());
if( status.getSpoiler_text() != null && status.getSpoiler_text().length() > 0 ){
toot_cw_content.setText(status.getSpoiler_text());
toot_cw_content.setVisibility(View.VISIBLE);
}else {
toot_cw_content.setText("");
toot_cw_content.setVisibility(View.GONE);
}
toot_content.setText(content);
toot_space_left.setText(String.valueOf(toot_content.length()));
toot_content.setSelection(toot_content.getText().length());
switch (status.getVisibility()){
case "public":
visibility = "public";
toot_visibility.setImageResource(R.drawable.ic_public_toot);
break;
case "unlisted":
visibility = "unlisted";
toot_visibility.setImageResource(R.drawable.ic_lock_open_toot);
break;
case "private":
visibility = "private";
toot_visibility.setImageResource(R.drawable.ic_lock_outline_toot);
break;
case "direct":
visibility = "direct";
toot_visibility.setImageResource(R.drawable.ic_mail_outline_toot);
break;
}
if( title != null)
title.setText(getString(R.string.toot_title));
else
setTitle(R.string.toot_title);
invalidateOptionsMenu();
initialContent = toot_content.getText().toString();
toot_space_left.setText(String.valueOf(toot_content.getText().length() + toot_cw_content.getText().length()));
}
private void tootReply(){ private void tootReply(){
SharedPreferences sharedpreferences = getSharedPreferences(Helper.APP_PREFS, android.content.Context.MODE_PRIVATE); SharedPreferences sharedpreferences = getSharedPreferences(Helper.APP_PREFS, android.content.Context.MODE_PRIVATE);
if( title != null) if( title != null)

View File

@ -25,6 +25,7 @@ import fr.gouv.etalab.mastodon.client.API;
import fr.gouv.etalab.mastodon.client.Entities.Account; import fr.gouv.etalab.mastodon.client.Entities.Account;
import fr.gouv.etalab.mastodon.client.Entities.Error; import fr.gouv.etalab.mastodon.client.Entities.Error;
import fr.gouv.etalab.mastodon.client.Entities.Results; import fr.gouv.etalab.mastodon.client.Entities.Results;
import fr.gouv.etalab.mastodon.client.Entities.StoredStatus;
import fr.gouv.etalab.mastodon.client.PeertubeAPI; import fr.gouv.etalab.mastodon.client.PeertubeAPI;
import fr.gouv.etalab.mastodon.helper.Helper; import fr.gouv.etalab.mastodon.helper.Helper;
import fr.gouv.etalab.mastodon.interfaces.OnPostActionInterface; import fr.gouv.etalab.mastodon.interfaces.OnPostActionInterface;
@ -48,6 +49,14 @@ public class PostActionAsyncTask extends AsyncTask<Void, Void, Void> {
private WeakReference<Context> contextReference; private WeakReference<Context> contextReference;
private boolean muteNotifications; private boolean muteNotifications;
private Error error; private Error error;
private StoredStatus storedStatus;
public PostActionAsyncTask(Context context, API.StatusAction apiAction, StoredStatus storedStatus, OnPostActionInterface onPostActionInterface){
this.contextReference = new WeakReference<>(context);
this.listener = onPostActionInterface;
this.apiAction = apiAction;
this.storedStatus = storedStatus;
}
public PostActionAsyncTask(Context context, API.StatusAction apiAction, String targetedId, OnPostActionInterface onPostActionInterface){ public PostActionAsyncTask(Context context, API.StatusAction apiAction, String targetedId, OnPostActionInterface onPostActionInterface){
this.contextReference = new WeakReference<>(context); this.contextReference = new WeakReference<>(context);
@ -154,7 +163,12 @@ public class PostActionAsyncTask extends AsyncTask<Void, Void, Void> {
statusCode = api.reportAction(status, comment); statusCode = api.reportAction(status, comment);
else if (apiAction == API.StatusAction.CREATESTATUS) else if (apiAction == API.StatusAction.CREATESTATUS)
statusCode = api.statusAction(status); statusCode = api.statusAction(status);
else if (apiAction == API.StatusAction.MUTE_NOTIFICATIONS) else if(apiAction == API.StatusAction.UPDATESERVERSCHEDULE) {
api.scheduledAction("PUT", storedStatus.getStatus(), null, storedStatus.getScheduledServerdId());
}
else if(apiAction == API.StatusAction.DELETESCHEDULED) {
api.scheduledAction("DELETE", null, null, storedStatus.getScheduledServerdId());
}else if (apiAction == API.StatusAction.MUTE_NOTIFICATIONS)
statusCode = api.muteNotifications(targetedId, muteNotifications); statusCode = api.muteNotifications(targetedId, muteNotifications);
else else
statusCode = api.postAction(apiAction, targetedId); statusCode = api.postAction(apiAction, targetedId);

View File

@ -170,7 +170,7 @@ public class RetrieveFeedsAsyncTask extends AsyncTask<Void, Void, Void> {
apiResponse = api.getPublicTimeline(false, max_id); apiResponse = api.getPublicTimeline(false, max_id);
break; break;
case SCHEDULED_TOOTS: case SCHEDULED_TOOTS:
apiResponse = api.scheduledAction("GET", null, max_id); apiResponse = api.scheduledAction("GET", null, max_id, null);
break; break;
case DIRECT: case DIRECT:
apiResponse = api.getDirectTimeline(max_id); apiResponse = api.getDirectTimeline(max_id);

View File

@ -120,7 +120,9 @@ public class API {
PEERTUBECOMMENT, PEERTUBECOMMENT,
PEERTUBEREPLY, PEERTUBEREPLY,
PEERTUBEDELETECOMMENT, PEERTUBEDELETECOMMENT,
PEERTUBEDELETEVIDEO PEERTUBEDELETEVIDEO,
UPDATESERVERSCHEDULE,
DELETESCHEDULED
} }
public enum accountPrivacy { public enum accountPrivacy {
@ -1856,33 +1858,12 @@ public class API {
* @param status Status object related to the status * @param status Status object related to the status
* @return APIResponse * @return APIResponse
*/ */
public APIResponse scheduledAction(String call, Status status, String max_id){ public APIResponse scheduledAction(String call, Status status, String max_id, String targetedId){
HashMap<String, String> params = new HashMap<>(); HashMap<String, String> params = new HashMap<>();
if( call.equals("PUT")){ if( call.equals("PUT")){
try { if( status.getScheduled_at() != null)
params.put("status", URLEncoder.encode(status.getContent(), "UTF-8")); params.put("scheduled_at", status.getScheduled_at());
} catch (UnsupportedEncodingException e) {
params.put("status", status.getContent());
}
if( status.getIn_reply_to_id() != null)
params.put("in_reply_to_id", status.getIn_reply_to_id());
if( status.getMedia_attachments() != null && status.getMedia_attachments().size() > 0 ) {
StringBuilder parameters = new StringBuilder();
for(Attachment attachment: status.getMedia_attachments())
parameters.append("media_ids[]=").append(attachment.getId()).append("&");
parameters = new StringBuilder(parameters.substring(0, parameters.length() - 1).substring(12));
params.put("media_ids[]", parameters.toString());
}
if( status.isSensitive())
params.put("sensitive", Boolean.toString(status.isSensitive()));
if( status.getSpoiler_text() != null)
try {
params.put("spoiler_text", URLEncoder.encode(status.getSpoiler_text(), "UTF-8"));
} catch (UnsupportedEncodingException e) {
params.put("spoiler_text", status.getSpoiler_text());
}
params.put("visibility", status.getVisibility());
}else if(call.equals("GET")){ }else if(call.equals("GET")){
if( max_id != null ) if( max_id != null )
params.put("max_id", max_id); params.put("max_id", max_id);
@ -1891,30 +1872,30 @@ public class API {
try { try {
HttpsConnection httpsConnection = new HttpsConnection(context); HttpsConnection httpsConnection = new HttpsConnection(context);
String response = null; String response = null;
int responseCode; int responseCode = -1;
if( call.equals("GET")) if( call.equals("GET"))
response = httpsConnection.get(getAbsoluteUrl("/scheduled_statuses/"), 60, null, prefKeyOauthTokenT); response = httpsConnection.get(getAbsoluteUrl("/scheduled_statuses/"), 60, null, prefKeyOauthTokenT);
else if( call.equals("PUT")) else if( call.equals("PUT"))
response = httpsConnection.get(getAbsoluteUrl(String.format("/scheduled_statuses/%s", status.getId())), 60, params, prefKeyOauthTokenT); response = httpsConnection.put(getAbsoluteUrl(String.format("/scheduled_statuses/%s", targetedId)), 60, params, prefKeyOauthTokenT);
else if( call.equals("DELETE")) else if( call.equals("DELETE"))
responseCode = httpsConnection.delete(getAbsoluteUrl(String.format("/scheduled_statuses/%s", status.getId())), 60, params, prefKeyOauthTokenT); responseCode = httpsConnection.delete(getAbsoluteUrl(String.format("/scheduled_statuses/%s",targetedId)), 60, null, prefKeyOauthTokenT);
if(call.equals("GET")) { if(call.equals("GET")) {
apiResponse.setSince_id(httpsConnection.getSince_id()); apiResponse.setSince_id(httpsConnection.getSince_id());
apiResponse.setMax_id(httpsConnection.getMax_id()); apiResponse.setMax_id(httpsConnection.getMax_id());
} }
if (response != null && call.equals("PUT")) { if (response != null && call.equals("PUT")) {
Status statusreturned = parseStatuses(context, new JSONObject(response)); Schedule schedule = parseSimpleSchedule(context, new JSONObject(response));
StoredStatus st = new StoredStatus(); StoredStatus st = new StoredStatus();
st.setCreation_date(status.getCreated_at()); st.setCreation_date(status.getCreated_at());
st.setId(-1); st.setId(-1);
st.setJobId(-1); st.setJobId(-1);
st.setScheduled_date(new Date(Long.parseLong(status.getScheduled_at()))); st.setScheduled_date(schedule.getScheduled_at());
st.setStatusReply(null); st.setStatusReply(null);
st.setSent_date(null); st.setSent_date(null);
final SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); final SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE);
String userId = sharedpreferences.getString(Helper.PREF_KEY_ID, null); String userId = sharedpreferences.getString(Helper.PREF_KEY_ID, null);
st.setUserId(userId); st.setUserId(userId);
st.setStatus(statusreturned); st.setStatus(schedule.getStatus());
storedStatus.add(st); storedStatus.add(st);
}else if (response != null && call.equals("GET")) { }else if (response != null && call.equals("GET")) {
List<Schedule> scheduleList = parseSchedule(context, new JSONArray(response)); List<Schedule> scheduleList = parseSchedule(context, new JSONArray(response));
@ -1923,7 +1904,7 @@ public class API {
for(Schedule schedule: scheduleList){ for(Schedule schedule: scheduleList){
StoredStatus st = new StoredStatus(); StoredStatus st = new StoredStatus();
st.setCreation_date(null); st.setCreation_date(null);
st.setId(-1); st.setScheduledServerdId(schedule.getId());
st.setJobId(-1); st.setJobId(-1);
st.setScheduled_date(schedule.getScheduled_at()); st.setScheduled_date(schedule.getScheduled_at());
st.setStatusReply(null); st.setStatusReply(null);
@ -3196,6 +3177,29 @@ public class API {
return conversation; return conversation;
} }
/**
* Parse json response for several scheduled toots
* @param jsonObject JSONObject
* @return List<Status>
*/
private static Schedule parseSimpleSchedule(Context context, JSONObject jsonObject){
Schedule schedule = new Schedule();
try {
JSONObject resobj = jsonObject.getJSONObject("params");
Status status = parseSchedule(context, resobj);
List<Attachment> attachements = parseAttachmentResponse(jsonObject.getJSONArray("media_attachments"));
status.setMedia_attachments((ArrayList<Attachment>) attachements);
schedule.setStatus(status);
schedule.setAttachmentList(attachements);
schedule.setId(jsonObject.get("id").toString());
schedule.setScheduled_at(Helper.mstStringToDate(context, jsonObject.get("scheduled_at").toString()));
} catch (JSONException e) {
e.printStackTrace();
} catch (ParseException e) {
e.printStackTrace();
}
return schedule;
}
/** /**
* Parse json response for several scheduled toots * Parse json response for several scheduled toots
@ -3217,11 +3221,13 @@ public class API {
schedule.setAttachmentList(attachements); schedule.setAttachmentList(attachements);
schedules.add(schedule); schedules.add(schedule);
schedule.setId(jsonArray.getJSONObject(i).get("id").toString()); schedule.setId(jsonArray.getJSONObject(i).get("id").toString());
schedule.setScheduled_at(Helper.stringToDate(context, resobj.get("scheduled_at").toString())); schedule.setScheduled_at(Helper.mstStringToDate(context, jsonArray.getJSONObject(i).get("scheduled_at").toString()));
i++; i++;
} }
} catch (JSONException e) { } catch (JSONException e) {
e.printStackTrace(); e.printStackTrace();
} catch (ParseException e) {
e.printStackTrace();
} }
return schedules; return schedules;
} }

View File

@ -15,6 +15,9 @@
package fr.gouv.etalab.mastodon.client.Entities; package fr.gouv.etalab.mastodon.client.Entities;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@ -23,7 +26,7 @@ import java.util.List;
* Manages scheduled toots * Manages scheduled toots
*/ */
public class Schedule { public class Schedule implements Parcelable {
private String id; private String id;
private Date scheduled_at; private Date scheduled_at;
@ -64,4 +67,37 @@ public class Schedule {
public void setAttachmentList(List<Attachment> attachmentList) { public void setAttachmentList(List<Attachment> attachmentList) {
this.attachmentList = attachmentList; this.attachmentList = attachmentList;
} }
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.id);
dest.writeLong(this.scheduled_at != null ? this.scheduled_at.getTime() : -1);
dest.writeParcelable(this.status, flags);
dest.writeTypedList(this.attachmentList);
}
protected Schedule(Parcel in) {
this.id = in.readString();
long tmpScheduled_at = in.readLong();
this.scheduled_at = tmpScheduled_at == -1 ? null : new Date(tmpScheduled_at);
this.status = in.readParcelable(Status.class.getClassLoader());
this.attachmentList = in.createTypedArrayList(Attachment.CREATOR);
}
public static final Parcelable.Creator<Schedule> CREATOR = new Parcelable.Creator<Schedule>() {
@Override
public Schedule createFromParcel(Parcel source) {
return new Schedule(source);
}
@Override
public Schedule[] newArray(int size) {
return new Schedule[size];
}
};
} }

View File

@ -1,5 +1,8 @@
package fr.gouv.etalab.mastodon.client.Entities; package fr.gouv.etalab.mastodon.client.Entities;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.Date; import java.util.Date;
@ -8,7 +11,7 @@ import java.util.Date;
* Manage Stored status * Manage Stored status
*/ */
public class StoredStatus { public class StoredStatus implements Parcelable {
private int id; private int id;
private Date creation_date; private Date creation_date;
@ -20,6 +23,7 @@ public class StoredStatus {
private Status statusReply; private Status statusReply;
private String instance; private String instance;
private String userId; private String userId;
private String scheduledServerdId;
public int getId() { public int getId() {
return id; return id;
@ -101,4 +105,65 @@ public class StoredStatus {
public void setStatusReply(Status statusReply) { public void setStatusReply(Status statusReply) {
this.statusReply = statusReply; this.statusReply = statusReply;
} }
public String getScheduledServerdId() {
return scheduledServerdId;
}
public void setScheduledServerdId(String scheduledServerdId) {
this.scheduledServerdId = scheduledServerdId;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.id);
dest.writeLong(this.creation_date != null ? this.creation_date.getTime() : -1);
dest.writeLong(this.scheduled_date != null ? this.scheduled_date.getTime() : -1);
dest.writeLong(this.sent_date != null ? this.sent_date.getTime() : -1);
dest.writeInt(this.jobId);
dest.writeByte(this.isSent ? (byte) 1 : (byte) 0);
dest.writeParcelable(this.status, flags);
dest.writeParcelable(this.statusReply, flags);
dest.writeString(this.instance);
dest.writeString(this.userId);
dest.writeString(this.scheduledServerdId);
}
public StoredStatus() {
}
protected StoredStatus(Parcel in) {
this.id = in.readInt();
long tmpCreation_date = in.readLong();
this.creation_date = tmpCreation_date == -1 ? null : new Date(tmpCreation_date);
long tmpScheduled_date = in.readLong();
this.scheduled_date = tmpScheduled_date == -1 ? null : new Date(tmpScheduled_date);
long tmpSent_date = in.readLong();
this.sent_date = tmpSent_date == -1 ? null : new Date(tmpSent_date);
this.jobId = in.readInt();
this.isSent = in.readByte() != 0;
this.status = in.readParcelable(Status.class.getClassLoader());
this.statusReply = in.readParcelable(Status.class.getClassLoader());
this.instance = in.readString();
this.userId = in.readString();
this.scheduledServerdId = in.readString();
}
public static final Creator<StoredStatus> CREATOR = new Creator<StoredStatus>() {
@Override
public StoredStatus createFromParcel(Parcel source) {
return new StoredStatus(source);
}
@Override
public StoredStatus[] newArray(int size) {
return new StoredStatus[size];
}
};
} }

View File

@ -20,6 +20,7 @@ import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
@ -49,10 +50,14 @@ import fr.gouv.etalab.mastodon.R;
import fr.gouv.etalab.mastodon.activities.MainActivity; import fr.gouv.etalab.mastodon.activities.MainActivity;
import fr.gouv.etalab.mastodon.activities.ShowConversationActivity; import fr.gouv.etalab.mastodon.activities.ShowConversationActivity;
import fr.gouv.etalab.mastodon.activities.TootActivity; import fr.gouv.etalab.mastodon.activities.TootActivity;
import fr.gouv.etalab.mastodon.asynctasks.PostActionAsyncTask;
import fr.gouv.etalab.mastodon.client.API;
import fr.gouv.etalab.mastodon.client.Entities.Error;
import fr.gouv.etalab.mastodon.client.Entities.Status; import fr.gouv.etalab.mastodon.client.Entities.Status;
import fr.gouv.etalab.mastodon.client.Entities.StoredStatus; import fr.gouv.etalab.mastodon.client.Entities.StoredStatus;
import fr.gouv.etalab.mastodon.fragments.DisplayScheduledTootsFragment; import fr.gouv.etalab.mastodon.fragments.DisplayScheduledTootsFragment;
import fr.gouv.etalab.mastodon.helper.Helper; import fr.gouv.etalab.mastodon.helper.Helper;
import fr.gouv.etalab.mastodon.interfaces.OnPostActionInterface;
import fr.gouv.etalab.mastodon.jobs.ApplicationJob; import fr.gouv.etalab.mastodon.jobs.ApplicationJob;
import fr.gouv.etalab.mastodon.jobs.ScheduledBoostsSyncJob; import fr.gouv.etalab.mastodon.jobs.ScheduledBoostsSyncJob;
import fr.gouv.etalab.mastodon.jobs.ScheduledTootsSyncJob; import fr.gouv.etalab.mastodon.jobs.ScheduledTootsSyncJob;
@ -67,7 +72,7 @@ import static fr.gouv.etalab.mastodon.helper.Helper.changeDrawableColor;
* Created by Thomas on 16/07/2017. * Created by Thomas on 16/07/2017.
* Adapter for scheduled toots * Adapter for scheduled toots
*/ */
public class ScheduledTootsListAdapter extends BaseAdapter { public class ScheduledTootsListAdapter extends BaseAdapter implements OnPostActionInterface {
private Context context; private Context context;
private List<StoredStatus> storedStatuses; private List<StoredStatus> storedStatuses;
@ -195,18 +200,27 @@ public class ScheduledTootsListAdapter extends BaseAdapter {
.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
if( type == DisplayScheduledTootsFragment.typeOfSchedule.TOOT) if( type != DisplayScheduledTootsFragment.typeOfSchedule.SERVER) {
new StatusStoredDAO(context, db).remove(storedStatus.getId()); if (type == DisplayScheduledTootsFragment.typeOfSchedule.TOOT)
else if (type == DisplayScheduledTootsFragment.typeOfSchedule.BOOST) new StatusStoredDAO(context, db).remove(storedStatus.getId());
new BoostScheduleDAO(context, db).remove(storedStatus.getId()); else if (type == DisplayScheduledTootsFragment.typeOfSchedule.BOOST)
storedStatuses.remove(storedStatus); new BoostScheduleDAO(context, db).remove(storedStatus.getId());
scheduledTootsListAdapter.notifyDataSetChanged(); storedStatuses.remove(storedStatus);
if( storedStatuses.size() == 0 && textviewNoAction != null && textviewNoAction.getVisibility() == View.GONE) scheduledTootsListAdapter.notifyDataSetChanged();
if (storedStatuses.size() == 0 && textviewNoAction != null && textviewNoAction.getVisibility() == View.GONE)
textviewNoAction.setVisibility(View.VISIBLE); textviewNoAction.setVisibility(View.VISIBLE);
try { try {
//Cancel the job //Cancel the job
ApplicationJob.cancelJob(storedStatus.getJobId()); ApplicationJob.cancelJob(storedStatus.getJobId());
}catch (Exception ignored){} } catch (Exception ignored) {
}
}else{
new PostActionAsyncTask(context, API.StatusAction.DELETESCHEDULED, storedStatus, ScheduledTootsListAdapter.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
storedStatuses.remove(storedStatus);
scheduledTootsListAdapter.notifyDataSetChanged();
if (storedStatuses.size() == 0 && textviewNoAction != null && textviewNoAction.getVisibility() == View.GONE)
textviewNoAction.setVisibility(View.VISIBLE);
}
dialog.dismiss(); dialog.dismiss();
} }
}) })
@ -221,10 +235,12 @@ public class ScheduledTootsListAdapter extends BaseAdapter {
} }
}); });
if (storedStatus.getJobId() > 0) { if( type != DisplayScheduledTootsFragment.typeOfSchedule.SERVER) {
holder.scheduled_toot_failed.setVisibility(View.GONE); if (storedStatus.getJobId() > 0) {
}else { holder.scheduled_toot_failed.setVisibility(View.GONE);
holder.scheduled_toot_failed.setVisibility(View.VISIBLE); } else {
holder.scheduled_toot_failed.setVisibility(View.VISIBLE);
}
} }
holder.scheduled_toot_media_count.setText(context.getString(R.string.media_count, status.getMedia_attachments().size())); holder.scheduled_toot_media_count.setText(context.getString(R.string.media_count, status.getMedia_attachments().size()));
holder.scheduled_toot_date_creation.setText(Helper.dateToString(storedStatus.getCreation_date())); holder.scheduled_toot_date_creation.setText(Helper.dateToString(storedStatus.getCreation_date()));
@ -319,28 +335,35 @@ public class ScheduledTootsListAdapter extends BaseAdapter {
Toasty.error(context, context.getString(R.string.toot_scheduled_date), Toast.LENGTH_LONG).show(); Toasty.error(context, context.getString(R.string.toot_scheduled_date), Toast.LENGTH_LONG).show();
}else { }else {
//Schedules the toot to the new date //Schedules the toot to the new date
try { if( type != DisplayScheduledTootsFragment.typeOfSchedule.SERVER) {
//Removes the job try {
ApplicationJob.cancelJob(storedStatus.getJobId()); //Removes the job
//Replace it by the new one ApplicationJob.cancelJob(storedStatus.getJobId());
StoredStatus storedStatusnew = null; //Replace it by the new one
if( type == DisplayScheduledTootsFragment.typeOfSchedule.TOOT) { StoredStatus storedStatusnew = null;
ScheduledTootsSyncJob.schedule(context, storedStatus.getId(), time); if (type == DisplayScheduledTootsFragment.typeOfSchedule.TOOT) {
storedStatusnew = new StatusStoredDAO(context, db).getStatus(storedStatus.getId()); ScheduledTootsSyncJob.schedule(context, storedStatus.getId(), time);
}else if(type == DisplayScheduledTootsFragment.typeOfSchedule.BOOST){ storedStatusnew = new StatusStoredDAO(context, db).getStatus(storedStatus.getId());
ScheduledBoostsSyncJob.scheduleUpdate(context, storedStatus.getId(), time); } else if (type == DisplayScheduledTootsFragment.typeOfSchedule.BOOST) {
storedStatusnew = new BoostScheduleDAO(context, db).getStatus(storedStatus.getId()); ScheduledBoostsSyncJob.scheduleUpdate(context, storedStatus.getId(), time);
storedStatusnew = new BoostScheduleDAO(context, db).getStatus(storedStatus.getId());
}
//Date displayed is changed
assert storedStatusnew != null;
storedStatus.setScheduled_date(storedStatusnew.getScheduled_date());
scheduledTootsListAdapter.notifyDataSetChanged();
//Notifiy all is ok
if (type == DisplayScheduledTootsFragment.typeOfSchedule.TOOT)
Toasty.success(context, context.getString(R.string.toot_scheduled), Toast.LENGTH_LONG).show();
else
Toasty.success(context, context.getString(R.string.boost_scheduled), Toast.LENGTH_LONG).show();
} catch (Exception ignored) {
} }
//Date displayed is changed }else{
assert storedStatusnew != null; storedStatus.getStatus().setScheduled_at(Helper.dateToString(calendar.getTime()));
storedStatus.setScheduled_date(storedStatusnew.getScheduled_date()); new PostActionAsyncTask(context, API.StatusAction.UPDATESERVERSCHEDULE, storedStatus, ScheduledTootsListAdapter.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
scheduledTootsListAdapter.notifyDataSetChanged(); Toasty.success(context, context.getString(R.string.boost_scheduled), Toast.LENGTH_LONG).show();
//Notifiy all is ok }
if( type == DisplayScheduledTootsFragment.typeOfSchedule.TOOT)
Toasty.success(context,context.getString(R.string.toot_scheduled), Toast.LENGTH_LONG).show();
else
Toasty.success(context,context.getString(R.string.boost_scheduled), Toast.LENGTH_LONG).show();
}catch (Exception ignored){}
alertDialog.dismiss(); alertDialog.dismiss();
} }
} }
@ -385,9 +408,27 @@ public class ScheduledTootsListAdapter extends BaseAdapter {
context.startActivity(intentToot); context.startActivity(intentToot);
} }
}); });
else if( type == DisplayScheduledTootsFragment.typeOfSchedule.SERVER)
holder.scheduled_toot_container.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intentToot = new Intent(context, TootActivity.class);
Bundle b = new Bundle();
if( storedStatus.getStatus().getSpoiler_text().equals("null"))
storedStatus.getStatus().setSpoiler_text("");
b.putParcelable("storedStatus", storedStatus);
intentToot.putExtras(b);
context.startActivity(intentToot);
}
});
return convertView; return convertView;
} }
@Override
public void onPostAction(int statusCode, API.StatusAction statusAction, String userId, Error error) {
}
private class ViewHolder { private class ViewHolder {
LinearLayout scheduled_toot_container; LinearLayout scheduled_toot_container;
TextView scheduled_toot_title; TextView scheduled_toot_title;

View File

@ -135,56 +135,59 @@ public class DisplayScheduledTootsFragment extends Fragment implements OnRetriev
@Override @Override
public void onResume(){ public void onResume(){
super.onResume(); super.onResume();
//Retrieves scheduled toots if( type != null && type != typeOfSchedule.SERVER) {
asyncTask = new RetrieveScheduledTootsAsyncTask(context, type,DisplayScheduledTootsFragment.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); //Retrieves scheduled toots
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { asyncTask = new RetrieveScheduledTootsAsyncTask(context, type, DisplayScheduledTootsFragment.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
final PowerManager powerManager = (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
final SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); final PowerManager powerManager = (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE);
//Battery saver is one and user never asked to stop showing the message final SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE);
changeDrawableColor(context, R.drawable.ic_report, R.color.mastodonC4); //Battery saver is one and user never asked to stop showing the message
changeDrawableColor(context, R.drawable.ic_cancel, R.color.mastodonC4); changeDrawableColor(context, R.drawable.ic_report, R.color.mastodonC4);
if( powerManager != null && powerManager.isPowerSaveMode() && sharedpreferences.getBoolean(Helper.SHOW_BATTERY_SAVER_MESSAGE,true)){ changeDrawableColor(context, R.drawable.ic_cancel, R.color.mastodonC4);
warning_battery_message.setVisibility(View.VISIBLE); if (powerManager != null && powerManager.isPowerSaveMode() && sharedpreferences.getBoolean(Helper.SHOW_BATTERY_SAVER_MESSAGE, true)) {
}else { warning_battery_message.setVisibility(View.VISIBLE);
warning_battery_message.setVisibility(View.GONE); } else {
} warning_battery_message.setVisibility(View.GONE);
warning_battery_message.setOnTouchListener(new View.OnTouchListener() { }
@Override warning_battery_message.setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) { @Override
final int DRAWABLE_RIGHT = 2; public boolean onTouch(View v, MotionEvent event) {
if(event.getAction() == MotionEvent.ACTION_UP) { final int DRAWABLE_RIGHT = 2;
if(event.getRawX() >= (warning_battery_message.getRight() - warning_battery_message.getCompoundDrawables()[DRAWABLE_RIGHT].getBounds().width())) { if (event.getAction() == MotionEvent.ACTION_UP) {
SharedPreferences.Editor editor = sharedpreferences.edit(); if (event.getRawX() >= (warning_battery_message.getRight() - warning_battery_message.getCompoundDrawables()[DRAWABLE_RIGHT].getBounds().width())) {
editor.putBoolean(Helper.SHOW_BATTERY_SAVER_MESSAGE, false); SharedPreferences.Editor editor = sharedpreferences.edit();
editor.apply(); editor.putBoolean(Helper.SHOW_BATTERY_SAVER_MESSAGE, false);
warning_battery_message.setVisibility(View.GONE); editor.apply();
return true; warning_battery_message.setVisibility(View.GONE);
return true;
}
}
return false;
}
});
warning_battery_message.setOnClickListener(new View.OnClickListener() {
@SuppressLint("BatteryLife")
@Override
public void onClick(View v) {
try {
Intent battSaverIntent = new Intent();
battSaverIntent.setComponent(new ComponentName("com.android.settings", "com.android.settings.Settings$BatterySaverSettingsActivity"));
startActivityForResult(battSaverIntent, 0);
} catch (ActivityNotFoundException e) {
try {
Intent batterySaverIntent;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1) {
batterySaverIntent = new Intent(Settings.ACTION_BATTERY_SAVER_SETTINGS);
startActivity(batterySaverIntent);
}
} catch (ActivityNotFoundException ignored) {
}
} }
} }
return false; });
} } else {
}); warning_battery_message.setVisibility(View.GONE);
warning_battery_message.setOnClickListener(new View.OnClickListener() { }
@SuppressLint("BatteryLife")
@Override
public void onClick(View v) {
try {
Intent battSaverIntent = new Intent();
battSaverIntent.setComponent(new ComponentName("com.android.settings", "com.android.settings.Settings$BatterySaverSettingsActivity"));
startActivityForResult(battSaverIntent, 0);
}catch (ActivityNotFoundException e){
try {
Intent batterySaverIntent;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1) {
batterySaverIntent = new Intent(Settings.ACTION_BATTERY_SAVER_SETTINGS);
startActivity(batterySaverIntent);
}
}catch (ActivityNotFoundException ignored){}
}
}
});
}else {
warning_battery_message.setVisibility(View.GONE);
} }
} }

View File

@ -833,6 +833,7 @@
<string name="server_schedule">From server</string> <string name="server_schedule">From server</string>
<string name="toots_server">Toots (Server)</string> <string name="toots_server">Toots (Server)</string>
<string name="toots_client">Toots (Device)</string> <string name="toots_client">Toots (Device)</string>
<string name="modify">Modify</string>
<!-- end languages --> <!-- end languages -->