/*
 * Copyright (C) 2010 The Android Open Source 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.android.server.content;

import android.accounts.Account;
import android.app.job.JobInfo;
import android.content.pm.PackageManager;
import android.content.ContentResolver;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.os.UserHandle;
import android.util.Slog;

/**
 * Value type that represents a sync operation.
 * This holds all information related to a sync operation - both one off and periodic.
 * Data stored in this is used to schedule a job with the JobScheduler.
 * {@hide}
 */
public class SyncOperation {
    public static final String TAG = "SyncManager";

    /**
     * This is used in the {@link #sourcePeriodicId} field if the operation is not initiated by a failed
     * periodic sync.
     */
    public static final int NO_JOB_ID = -1;

    public static final int REASON_BACKGROUND_DATA_SETTINGS_CHANGED = -1;
    public static final int REASON_ACCOUNTS_UPDATED = -2;
    public static final int REASON_SERVICE_CHANGED = -3;
    public static final int REASON_PERIODIC = -4;
    /** Sync started because it has just been set to isSyncable. */
    public static final int REASON_IS_SYNCABLE = -5;
    /** Sync started because it has just been set to sync automatically. */
    public static final int REASON_SYNC_AUTO = -6;
    /** Sync started because master sync automatically has been set to true. */
    public static final int REASON_MASTER_SYNC_AUTO = -7;
    public static final int REASON_USER_START = -8;

    private static String[] REASON_NAMES = new String[] {
            "DataSettingsChanged",
            "AccountsUpdated",
            "ServiceChanged",
            "Periodic",
            "IsSyncable",
            "AutoSync",
            "MasterSyncAuto",
            "UserStart",
    };

    /** Identifying info for the target for this operation. */
    public final SyncStorageEngine.EndPoint target;
    public final int owningUid;
    public final String owningPackage;
    /** Why this sync was kicked off. {@link #REASON_NAMES} */
    public final int reason;
    /** Where this sync was initiated. */
    public final int syncSource;
    public final boolean allowParallelSyncs;
    public final Bundle extras;
    public final boolean isPeriodic;
    /** jobId of the periodic SyncOperation that initiated this one */
    public final int sourcePeriodicId;
    /** Operations are considered duplicates if keys are equal */
    public final String key;

    /** Poll frequency of periodic sync in milliseconds */
    public final long periodMillis;
    /** Flex time of periodic sync in milliseconds */
    public final long flexMillis;
    /** Descriptive string key for this operation */
    public String wakeLockName;
    /**
     * Used when duplicate pending syncs are present. The one with the lowest expectedRuntime
     * is kept, others are discarded.
     */
    public long expectedRuntime;

    /** Stores the number of times this sync operation failed and had to be retried. */
    int retries;

    /** jobId of the JobScheduler job corresponding to this sync */
    public int jobId;

    public SyncOperation(Account account, int userId, int owningUid, String owningPackage,
                         int reason, int source, String provider, Bundle extras,
                         boolean allowParallelSyncs) {
        this(new SyncStorageEngine.EndPoint(account, provider, userId), owningUid, owningPackage,
                reason, source, extras, allowParallelSyncs);
    }

    private SyncOperation(SyncStorageEngine.EndPoint info, int owningUid, String owningPackage,
                          int reason, int source, Bundle extras, boolean allowParallelSyncs) {
        this(info, owningUid, owningPackage, reason, source, extras, allowParallelSyncs, false,
                NO_JOB_ID, 0, 0);
    }

    public SyncOperation(SyncOperation op, long periodMillis, long flexMillis) {
        this(op.target, op.owningUid, op.owningPackage, op.reason, op.syncSource,
                new Bundle(op.extras), op.allowParallelSyncs, op.isPeriodic, op.sourcePeriodicId,
                periodMillis, flexMillis);
    }

    public SyncOperation(SyncStorageEngine.EndPoint info, int owningUid, String owningPackage,
                         int reason, int source, Bundle extras, boolean allowParallelSyncs,
                         boolean isPeriodic, int sourcePeriodicId, long periodMillis,
                         long flexMillis) {
        this.target = info;
        this.owningUid = owningUid;
        this.owningPackage = owningPackage;
        this.reason = reason;
        this.syncSource = source;
        this.extras = new Bundle(extras);
        this.allowParallelSyncs = allowParallelSyncs;
        this.isPeriodic = isPeriodic;
        this.sourcePeriodicId = sourcePeriodicId;
        this.periodMillis = periodMillis;
        this.flexMillis = flexMillis;
        this.jobId = NO_JOB_ID;
        this.key = toKey();
    }

    /* Get a one off sync operation instance from a periodic sync. */
    public SyncOperation createOneTimeSyncOperation() {
        if (!isPeriodic) {
            return null;
        }
        SyncOperation op = new SyncOperation(target, owningUid, owningPackage, reason, syncSource,
                new Bundle(extras), allowParallelSyncs, false, jobId /* sourcePeriodicId */,
                periodMillis, flexMillis);
        return op;
    }

    public SyncOperation(SyncOperation other) {
        target = other.target;
        owningUid = other.owningUid;
        owningPackage = other.owningPackage;
        reason = other.reason;
        syncSource = other.syncSource;
        allowParallelSyncs = other.allowParallelSyncs;
        extras = new Bundle(other.extras);
        wakeLockName = other.wakeLockName();
        isPeriodic = other.isPeriodic;
        sourcePeriodicId = other.sourcePeriodicId;
        periodMillis = other.periodMillis;
        flexMillis = other.flexMillis;
        this.key = other.key;
    }

    /**
     * All fields are stored in a corresponding key in the persistable bundle.
     *
     * {@link #extras} is a Bundle and can contain parcelable objects. But only the type Account
     * is allowed {@link ContentResolver#validateSyncExtrasBundle(Bundle)} that can't be stored in
     * a PersistableBundle. For every value of type Account with key 'key', we store a
     * PersistableBundle containing account information at key 'ACCOUNT:key'. The Account object
     * can be reconstructed using this.
     *
     * We put a flag with key 'SyncManagerJob', to identify while reconstructing a sync operation
     * from a bundle whether the bundle actually contains information about a sync.
     * @return A persistable bundle containing all information to re-construct the sync operation.
     */
    PersistableBundle toJobInfoExtras() {
        // This will be passed as extras bundle to a JobScheduler job.
        PersistableBundle jobInfoExtras = new PersistableBundle();

        PersistableBundle syncExtrasBundle = new PersistableBundle();
        for (String key: extras.keySet()) {
            Object value = extras.get(key);
            if (value instanceof Account) {
                Account account = (Account) value;
                PersistableBundle accountBundle = new PersistableBundle();
                accountBundle.putString("accountName", account.name);
                accountBundle.putString("accountType", account.type);
                // This is stored in jobInfoExtras so that we don't override a user specified
                // sync extra with the same key.
                jobInfoExtras.putPersistableBundle("ACCOUNT:" + key, accountBundle);
            } else if (value instanceof Long) {
                syncExtrasBundle.putLong(key, (Long) value);
            } else if (value instanceof Integer) {
                syncExtrasBundle.putInt(key, (Integer) value);
            } else if (value instanceof Boolean) {
                syncExtrasBundle.putBoolean(key, (Boolean) value);
            } else if (value instanceof Float) {
                syncExtrasBundle.putDouble(key, (Double) value);
            } else if (value instanceof Double) {
                syncExtrasBundle.putDouble(key, (Double) value);
            } else if (value instanceof String) {
                syncExtrasBundle.putString(key, (String) value);
            } else if (value == null) {
                syncExtrasBundle.putString(key, null);
            } else {
                Slog.e(TAG, "Unknown extra type.");
            }
        }
        jobInfoExtras.putPersistableBundle("syncExtras", syncExtrasBundle);

        jobInfoExtras.putBoolean("SyncManagerJob", true);

        jobInfoExtras.putString("provider", target.provider);
        jobInfoExtras.putString("accountName", target.account.name);
        jobInfoExtras.putString("accountType", target.account.type);
        jobInfoExtras.putInt("userId", target.userId);
        jobInfoExtras.putInt("owningUid", owningUid);
        jobInfoExtras.putString("owningPackage", owningPackage);
        jobInfoExtras.putInt("reason", reason);
        jobInfoExtras.putInt("source", syncSource);
        jobInfoExtras.putBoolean("allowParallelSyncs", allowParallelSyncs);
        jobInfoExtras.putInt("jobId", jobId);
        jobInfoExtras.putBoolean("isPeriodic", isPeriodic);
        jobInfoExtras.putInt("sourcePeriodicId", sourcePeriodicId);
        jobInfoExtras.putLong("periodMillis", periodMillis);
        jobInfoExtras.putLong("flexMillis", flexMillis);
        jobInfoExtras.putLong("expectedRuntime", expectedRuntime);
        jobInfoExtras.putInt("retries", retries);
        return jobInfoExtras;
    }

    /**
     * Reconstructs a sync operation from an extras Bundle. Returns null if the bundle doesn't
     * contain a valid sync operation.
     */
    static SyncOperation maybeCreateFromJobExtras(PersistableBundle jobExtras) {
        String accountName, accountType;
        String provider;
        int userId, owningUid;
        String owningPackage;
        int reason, source;
        int initiatedBy;
        Bundle extras;
        boolean allowParallelSyncs, isPeriodic;
        long periodMillis, flexMillis;

        if (!jobExtras.getBoolean("SyncManagerJob", false)) {
            return null;
        }

        accountName = jobExtras.getString("accountName");
        accountType = jobExtras.getString("accountType");
        provider = jobExtras.getString("provider");
        userId = jobExtras.getInt("userId", Integer.MAX_VALUE);
        owningUid = jobExtras.getInt("owningUid");
        owningPackage = jobExtras.getString("owningPackage");
        reason = jobExtras.getInt("reason", Integer.MAX_VALUE);
        source = jobExtras.getInt("source", Integer.MAX_VALUE);
        allowParallelSyncs = jobExtras.getBoolean("allowParallelSyncs", false);
        isPeriodic = jobExtras.getBoolean("isPeriodic", false);
        initiatedBy = jobExtras.getInt("sourcePeriodicId", NO_JOB_ID);
        periodMillis = jobExtras.getLong("periodMillis");
        flexMillis = jobExtras.getLong("flexMillis");
        extras = new Bundle();

        PersistableBundle syncExtras = jobExtras.getPersistableBundle("syncExtras");
        if (syncExtras != null) {
            extras.putAll(syncExtras);
        }

        for (String key: jobExtras.keySet()) {
            if (key!= null && key.startsWith("ACCOUNT:")) {
                String newKey = key.substring(8); // Strip off the 'ACCOUNT:' prefix.
                PersistableBundle accountsBundle = jobExtras.getPersistableBundle(key);
                Account account = new Account(accountsBundle.getString("accountName"),
                        accountsBundle.getString("accountType"));
                extras.putParcelable(newKey, account);
            }
        }

        Account account = new Account(accountName, accountType);
        SyncStorageEngine.EndPoint target =
                new SyncStorageEngine.EndPoint(account, provider, userId);
        SyncOperation op = new SyncOperation(target, owningUid, owningPackage, reason, source,
                extras, allowParallelSyncs, isPeriodic, initiatedBy, periodMillis, flexMillis);
        op.jobId = jobExtras.getInt("jobId");
        op.expectedRuntime = jobExtras.getLong("expectedRuntime");
        op.retries = jobExtras.getInt("retries");
        return op;
    }

    /**
     * Determine whether if this sync operation is running, the provided operation would conflict
     * with it.
     * Parallel syncs allow multiple accounts to be synced at the same time.
     */
    boolean isConflict(SyncOperation toRun) {
        final SyncStorageEngine.EndPoint other = toRun.target;
        return target.account.type.equals(other.account.type)
                && target.provider.equals(other.provider)
                && target.userId == other.userId
                && (!allowParallelSyncs
                || target.account.name.equals(other.account.name));
    }

    boolean isReasonPeriodic() {
        return reason == REASON_PERIODIC;
    }

    boolean matchesPeriodicOperation(SyncOperation other) {
        return target.matchesSpec(other.target)
                && SyncManager.syncExtrasEquals(extras, other.extras, true)
                && periodMillis == other.periodMillis && flexMillis == other.flexMillis;
    }

    boolean isDerivedFromFailedPeriodicSync() {
        return sourcePeriodicId != NO_JOB_ID;
    }

    int findPriority() {
        if (isInitialization()) {
            return JobInfo.PRIORITY_SYNC_INITIALIZATION;
        } else if (isExpedited()) {
            return JobInfo.PRIORITY_SYNC_EXPEDITED;
        }
        return JobInfo.PRIORITY_DEFAULT;
    }

    private String toKey() {
        StringBuilder sb = new StringBuilder();
        sb.append("provider: ").append(target.provider);
        sb.append(" account {name=" + target.account.name
                + ", user="
                + target.userId
                + ", type="
                + target.account.type
                + "}");
        sb.append(" isPeriodic: ").append(isPeriodic);
        sb.append(" period: ").append(periodMillis);
        sb.append(" flex: ").append(flexMillis);
        sb.append(" extras: ");
        extrasToStringBuilder(extras, sb);
        return sb.toString();
    }

    @Override
    public String toString() {
        return dump(null, true);
    }

    String dump(PackageManager pm, boolean useOneLine) {
        StringBuilder sb = new StringBuilder();
        sb.append("JobId: ").append(jobId)
                .append(", ")
                .append(target.account.name)
                .append(" u")
                .append(target.userId).append(" (")
                .append(target.account.type)
                .append(")")
                .append(", ")
                .append(target.provider)
                .append(", ");
        sb.append(SyncStorageEngine.SOURCES[syncSource]);
        if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false)) {
            sb.append(", EXPEDITED");
        }
        sb.append(", reason: ");
        sb.append(reasonToString(pm, reason));
        if (isPeriodic) {
            sb.append(", period: " + periodMillis).append(", flexMillis: " + flexMillis);
        }
        if (!useOneLine) {
            sb.append("\n    ");
            sb.append("owningUid=");
            UserHandle.formatUid(sb, owningUid);
            sb.append(" owningPackage=");
            sb.append(owningPackage);
        }
        if (!useOneLine && !extras.keySet().isEmpty()) {
            sb.append("\n    ");
            extrasToStringBuilder(extras, sb);
        }
        return sb.toString();
    }

    static String reasonToString(PackageManager pm, int reason) {
        if (reason >= 0) {
            if (pm != null) {
                final String[] packages = pm.getPackagesForUid(reason);
                if (packages != null && packages.length == 1) {
                    return packages[0];
                }
                final String name = pm.getNameForUid(reason);
                if (name != null) {
                    return name;
                }
                return String.valueOf(reason);
            } else {
                return String.valueOf(reason);
            }
        } else {
            final int index = -reason - 1;
            if (index >= REASON_NAMES.length) {
                return String.valueOf(reason);
            } else {
                return REASON_NAMES[index];
            }
        }
    }

    boolean isInitialization() {
        return extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false);
    }

    boolean isExpedited() {
        return extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false);
    }

    boolean ignoreBackoff() {
        return extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false);
    }

    boolean isNotAllowedOnMetered() {
        return extras.getBoolean(ContentResolver.SYNC_EXTRAS_DISALLOW_METERED, false);
    }

    boolean isManual() {
        return extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
    }

    boolean isIgnoreSettings() {
        return extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false);
    }

    private static void extrasToStringBuilder(Bundle bundle, StringBuilder sb) {
        sb.append("[");
        for (String key : bundle.keySet()) {
            sb.append(key).append("=").append(bundle.get(key)).append(" ");
        }
        sb.append("]");
    }

    String wakeLockName() {
        if (wakeLockName != null) {
            return wakeLockName;
        }
        return (wakeLockName = target.provider
                + "/" + target.account.type
                + "/" + target.account.name);
    }

    // TODO: Test this to make sure that casting to object doesn't lose the type info for EventLog.
    public Object[] toEventLog(int event) {
        Object[] logArray = new Object[4];
        logArray[1] = event;
        logArray[2] = syncSource;
        logArray[0] = target.provider;
        logArray[3] = target.account.name.hashCode();
        return logArray;
    }
}
